/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2020 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.opencmis;

import static java.util.Collections.singletonList;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;

import org.apache.chemistry.opencmis.commons.BasicPermissions;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectList;
import org.apache.chemistry.opencmis.commons.data.PermissionMapping;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.apache.chemistry.opencmis.commons.data.PropertyId;
import org.apache.chemistry.opencmis.commons.data.PropertyString;
import org.apache.chemistry.opencmis.commons.data.RenditionData;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.definitions.DocumentTypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PermissionDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
import org.apache.chemistry.opencmis.commons.enums.Action;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.CapabilityAcl;
import org.apache.chemistry.opencmis.commons.enums.CapabilityChanges;
import org.apache.chemistry.opencmis.commons.enums.CapabilityContentStreamUpdates;
import org.apache.chemistry.opencmis.commons.enums.CapabilityJoin;
import org.apache.chemistry.opencmis.commons.enums.CapabilityQuery;
import org.apache.chemistry.opencmis.commons.enums.CapabilityRenditions;
import org.apache.chemistry.opencmis.commons.enums.Cardinality;
import org.apache.chemistry.opencmis.commons.enums.ChangeType;
import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
import org.apache.chemistry.opencmis.commons.enums.ContentStreamAllowed;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.PropertyType;
import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
import org.apache.chemistry.opencmis.commons.enums.SupportedPermissions;
import org.apache.chemistry.opencmis.commons.enums.Updatability;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlEntryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AclCapabilitiesDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.CmisExtensionElementImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionDefinitionDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionMappingDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PolicyIdListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyBooleanImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDateTimeImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyHtmlImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIntegerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyUriImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryCapabilitiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.server.CmisService;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.util.StringUtils;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.opencmis.dictionary.CMISActionEvaluator;
import org.alfresco.opencmis.dictionary.CMISAllowedActionEnum;
import org.alfresco.opencmis.dictionary.CMISDictionaryService;
import org.alfresco.opencmis.dictionary.CMISNodeInfo;
import org.alfresco.opencmis.dictionary.CMISObjectVariant;
import org.alfresco.opencmis.dictionary.CMISPropertyAccessor;
import org.alfresco.opencmis.dictionary.DocumentTypeDefinitionWrapper;
import org.alfresco.opencmis.dictionary.FolderTypeDefintionWrapper;
import org.alfresco.opencmis.dictionary.ItemTypeDefinitionWrapper;
import org.alfresco.opencmis.dictionary.PropertyDefinitionWrapper;
import org.alfresco.opencmis.dictionary.TypeDefinitionWrapper;
import org.alfresco.opencmis.search.CMISQueryOptions;
import org.alfresco.opencmis.search.CMISQueryOptions.CMISQueryMode;
import org.alfresco.opencmis.search.CMISQueryService;
import org.alfresco.opencmis.search.CMISResultSet;
import org.alfresco.opencmis.search.CMISResultSetColumn;
import org.alfresco.opencmis.search.CMISResultSetRow;
import org.alfresco.repo.action.executer.ContentMetadataExtracter;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.coci.CheckOutCheckInServiceImpl;
import org.alfresco.repo.model.filefolder.GetChildrenCannedQuery;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.security.permissions.PermissionReference;
import org.alfresco.repo.security.permissions.impl.AccessPermissionImpl;
import org.alfresco.repo.security.permissions.impl.ModelDAO;
import org.alfresco.repo.tenant.TenantAdminService;
import org.alfresco.repo.tenant.TenantDeployer;
import org.alfresco.repo.thumbnail.ThumbnailDefinition;
import org.alfresco.repo.thumbnail.ThumbnailHelper;
import org.alfresco.repo.thumbnail.ThumbnailRegistry;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.version.VersionBaseModel;
import org.alfresco.repo.version.VersionModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.activities.ActivityInfo;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.InvalidAspectException;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.Path.ChildAssocElement;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.thumbnail.ThumbnailService;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.cmr.version.VersionType;
import org.alfresco.service.descriptor.Descriptor;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.sync.events.types.ContentEvent;
import org.alfresco.sync.events.types.ContentEventImpl;
import org.alfresco.sync.events.types.ContentReadRangeEvent;
import org.alfresco.sync.events.types.Event;
import org.alfresco.sync.repo.Client;
import org.alfresco.sync.repo.Client.ClientType;
import org.alfresco.sync.repo.events.EventPreparator;
import org.alfresco.sync.repo.events.EventPublisher;
import org.alfresco.util.FileFilterMode;
import org.alfresco.util.Pair;
import org.alfresco.util.TempFileProvider;

/**
 * Bridge connecting Alfresco and OpenCMIS.
 * <p/>
 * This class provides many of the typical services that the {@link CmisService} implementation will need to use Alfresco.
 *
 * @author florian.mueller
 * @author Derek Hulley
 * @author steveglover
 */
public class CMISConnector implements ApplicationContextAware, ApplicationListener<ApplicationContextEvent>, TenantDeployer
{
    private static Log logger = LogFactory.getLog(CMISConnector.class);

    // mappings from cmis property names to their Alfresco property name counterparts (used by getChildren)
    private static Map<String, QName> SORT_PROPERTY_MAPPINGS = new HashMap<String, QName>();

    static
    {
        SORT_PROPERTY_MAPPINGS.put(PropertyIds.LAST_MODIFICATION_DATE, ContentModel.PROP_MODIFIED);
        SORT_PROPERTY_MAPPINGS.put(PropertyIds.CREATION_DATE, ContentModel.PROP_CREATED);
    }

    public static final char ID_SEPERATOR = ';';
    public static final char REPLACEMENT_CHAR = '_';
    public static final String ASSOC_ID_PREFIX = "assoc:";
    public static final String PWC_VERSION_LABEL = "pwc";
    public static final String UNVERSIONED_VERSION_LABEL = "1.0";

    public static final String RENDITION_NONE = "cmis:none";

    public static final String CMIS_CHANGELOG_AUDIT_APPLICATION = "CMISChangeLog";

    public static final String ALFRESCO_EXTENSION_NAMESPACE = "http://www.alfresco.org";
    public static final String CMIS_NAMESPACE = "http://docs.oasis-open.org/ns/cmis/core/200908/";
    public static final String ASPECTS = "aspects";
    public static final String SET_ASPECTS = "setAspects";
    public static final String APPLIED_ASPECTS = "appliedAspects";
    public static final String ASPECTS_TO_ADD = "aspectsToAdd";
    public static final String ASPECTS_TO_REMOVE = "aspectsToRemove";
    public static final String PROPERTIES = "properties";

    private static final BigInteger TYPES_DEFAULT_MAX_ITEMS = BigInteger.valueOf(50);
    private static final BigInteger TYPES_DEFAULT_DEPTH = BigInteger.valueOf(-1);
    private static final BigInteger OBJECTS_DEFAULT_MAX_ITEMS = BigInteger.valueOf(200);
    private static final BigInteger OBJECTS_DEFAULT_DEPTH = BigInteger.valueOf(10);
    private static final int CONTENT_CHANGES_DEFAULT_MAX_ITEMS = 10000;

    private static final String QUERY_NAME_OBJECT_ID = "cmis:objectId";
    private static final String QUERY_NAME_OBJECT_TYPE_ID = "cmis:objectTypeId";
    private static final String QUERY_NAME_BASE_TYPE_ID = "cmis:baseTypeId";

    private static final String CMIS_USER = "cmis:user";

    private static final BigInteger maxInt = BigInteger.valueOf(Integer.MAX_VALUE);
    private static final BigInteger minInt = BigInteger.valueOf(Integer.MIN_VALUE);
    private static final BigInteger maxLong = BigInteger.valueOf(Long.MAX_VALUE);
    private static final BigInteger minLong = BigInteger.valueOf(Long.MIN_VALUE);

    // lifecycle
    private ProcessorLifecycle lifecycle = new ProcessorLifecycle();

    // Alfresco objects
    private DescriptorService descriptorService;
    private NodeService nodeService;
    private VersionService versionService;
    private CheckOutCheckInService checkOutCheckInService;
    private LockService lockService;
    private ContentService contentService;
    @Deprecated
    private RenditionService renditionService;
    private FileFolderService fileFolderService;
    private TenantAdminService tenantAdminService;
    private TransactionService transactionService;
    private AuthenticationService authenticationService;
    private PermissionService permissionService;
    private ModelDAO permissionModelDao;
    private CMISDictionaryService cmisDictionaryService;
    private CMISDictionaryService cmisDictionaryService11;
    private CMISQueryService cmisQueryService;
    private CMISQueryService cmisQueryService11;
    private MimetypeService mimetypeService;
    private AuditService auditService;
    private NamespaceService namespaceService;
    private SearchService searchService;
    private DictionaryService dictionaryService;
    private SiteService siteService;
    private ActionService actionService;
    @Deprecated
    private ThumbnailService thumbnailService;
    private ServiceRegistry serviceRegistry;
    private EventPublisher eventPublisher;

    private CmisActivityPoster activityPoster;

    private BehaviourFilter behaviourFilter;

    private HiddenAspect hiddenAspect;

    private StoreRef storeRef;
    private String rootPath;
    private Map<String, List<String>> kindToRenditionNames;

    // note: cache is tenant-aware (if using TransctionalCache impl)

    private SimpleCache<String, Object> singletonCache; // eg. for cmisRootNodeRef, cmisRenditionMapping
    private final String KEY_CMIS_ROOT_NODEREF = "key.cmisRoot.noderef";
    private final String KEY_CMIS_RENDITION_MAPPING_NODEREF = "key.cmisRenditionMapping.noderef";

    private String proxyUser;
    private boolean openHttpSession = false;

    // OpenCMIS objects
    private BigInteger typesDefaultMaxItems = TYPES_DEFAULT_MAX_ITEMS;
    private BigInteger typesDefaultDepth = TYPES_DEFAULT_DEPTH;
    private BigInteger objectsDefaultMaxItems = OBJECTS_DEFAULT_MAX_ITEMS;
    private BigInteger objectsDefaultDepth = OBJECTS_DEFAULT_DEPTH;
    private int contentChangesDefaultMaxItems = CONTENT_CHANGES_DEFAULT_MAX_ITEMS;

    private List<PermissionDefinition> repositoryPermissions;
    private Map<String, PermissionMapping> permissionMappings;

    private ObjectFilter objectFilter;

    // Bulk update properties
    private int bulkMaxItems = 1000;
    private int bulkBatchSize = 20;
    private int bulkWorkerThreads = 2;

    // --------------------------------------------------------------
    // Configuration
    // --------------------------------------------------------------

    public void setObjectFilter(ObjectFilter objectFilter)
    {
        this.objectFilter = objectFilter;
    }

    /**
     * Sets the root store.
     *
     * @param store
     *            store_type://store_id
     */
    public void setStore(String store)
    {
        this.storeRef = new StoreRef(store);
    }

    public void setSiteService(SiteService siteService)
    {
        this.siteService = siteService;
    }

    public void setActivityPoster(CmisActivityPoster activityPoster)
    {
        this.activityPoster = activityPoster;
    }

    public CmisActivityPoster getActivityPoster()
    {
        return activityPoster;
    }

    public void setHiddenAspect(HiddenAspect hiddenAspect)
    {
        this.hiddenAspect = hiddenAspect;
    }

    public boolean isHidden(NodeRef nodeRef)
    {
        final FileFilterMode.Client client = FileFilterMode.getClient();
        return (hiddenAspect.getVisibility(client, nodeRef) == Visibility.NotVisible);
    }

    /**
     * Sets the root path.
     *
     * @param path
     *            path within default store
     */
    public void setRootPath(String path)
    {
        rootPath = path;
    }

    public BigInteger getTypesDefaultMaxItems()
    {
        return typesDefaultMaxItems;
    }

    public void setTypesDefaultMaxItems(BigInteger typesDefaultMaxItems)
    {
        this.typesDefaultMaxItems = typesDefaultMaxItems;
    }

    public BigInteger getTypesDefaultDepth()
    {
        return typesDefaultDepth;
    }

    public void setTypesDefaultDepth(BigInteger typesDefaultDepth)
    {
        this.typesDefaultDepth = typesDefaultDepth;
    }

    public BigInteger getObjectsDefaultMaxItems()
    {
        return objectsDefaultMaxItems;
    }

    public void setObjectsDefaultMaxItems(BigInteger objectsDefaultMaxItems)
    {
        this.objectsDefaultMaxItems = objectsDefaultMaxItems;
    }

    public BigInteger getObjectsDefaultDepth()
    {
        return objectsDefaultDepth;
    }

    public void setObjectsDefaultDepth(BigInteger objectsDefaultDepth)
    {
        this.objectsDefaultDepth = objectsDefaultDepth;
    }

    /**
     * Set the default number of content changes to return if nothing is specified
     */
    public void setContentChangesDefaultMaxItems(int contentChangesDefaultMaxItems)
    {
        if (contentChangesDefaultMaxItems < 1)
        {
            throw new IllegalArgumentException("The default maximum number of content changes to retrieve must be greater than zero.");
        }
        else if (contentChangesDefaultMaxItems == Integer.MAX_VALUE)
        {
            throw new IllegalArgumentException("The server cannot return " + Integer.MAX_VALUE + " content changes in a request!");
        }
        this.contentChangesDefaultMaxItems = contentChangesDefaultMaxItems;
    }

    /**
     * Set rendition kind mapping.
     */
    public void setRenditionKindMapping(Map<String, List<String>> renditionKinds)
    {
        this.kindToRenditionNames = renditionKinds;
    }

    public void setOpenHttpSession(boolean openHttpSession)
    {
        this.openHttpSession = openHttpSession;
    }

    public boolean openHttpSession()
    {
        return openHttpSession;
    }

    /**
     *
     *
     * @deprecated The thumbnails code is being moved out of the codebase and replaced by the new async RenditionService2 or other external libraries.
     */
    @Deprecated
    public void setThumbnailService(ThumbnailService thumbnailService)
    {
        this.thumbnailService = thumbnailService;
    }

    public void setServiceRegistry(ServiceRegistry serviceRegistry)
    {
        this.serviceRegistry = serviceRegistry;
    }

    /**
     * Return the service registry
     */
    public final ServiceRegistry getServiceRegistry()
    {
        return this.serviceRegistry;
    }

    /**
     * Sets the descriptor service.
     */
    public void setDescriptorService(DescriptorService descriptorService)
    {
        this.descriptorService = descriptorService;
    }

    public DescriptorService getDescriptorService()
    {
        return descriptorService;
    }

    public void setBehaviourFilter(BehaviourFilter behaviourFilter)
    {
        this.behaviourFilter = behaviourFilter;
    }

    /**
     * Sets the node service.
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    public NodeService getNodeService()
    {
        return nodeService;
    }

    public void setActionService(ActionService actionService)
    {
        this.actionService = actionService;
    }

    /**
     * Sets the version service.
     */
    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }

    public VersionService getVersionService()
    {
        return versionService;
    }

    /**
     * Sets the checkOut/checkIn service.
     */
    public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService)
    {
        this.checkOutCheckInService = checkOutCheckInService;
    }

    public CheckOutCheckInService getCheckOutCheckInService()
    {
        return checkOutCheckInService;
    }

    /**
     * Sets the lock service.
     */
    public LockService getLockService()
    {
        return lockService;
    }

    public void setLockService(LockService lockService)
    {
        this.lockService = lockService;
    }

    /**
     * Sets the content service.
     */
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }

    public ContentService getContentService()
    {
        return contentService;
    }

    /**
     * Sets the event publisher
     */
    public void setEventPublisher(EventPublisher eventPublisher)
    {
        this.eventPublisher = eventPublisher;
    }

    /**
     * Sets the rendition service.
     *
     * @deprecated The RenditionService is being replace by the simpler async RenditionService2.
     */
    @Deprecated
    public void setrenditionService(RenditionService renditionService)
    {
        this.renditionService = renditionService;
    }

    /**
     * Sets the file folder service.
     */
    public void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }

    public FileFolderService getFileFolderService()
    {
        return fileFolderService;
    }

    /**
     * Sets the tenant admin service.
     */
    public void setTenantAdminService(TenantAdminService tenantAdminService)
    {
        this.tenantAdminService = tenantAdminService;
    }

    public void setSingletonCache(SimpleCache<String, Object> singletonCache)
    {
        this.singletonCache = singletonCache;
    }

    /**
     * Sets the transaction service.
     */
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }

    public TransactionService getTransactionService()
    {
        return transactionService;
    }

    /**
     * Sets the authentication service.
     */
    public void setAuthenticationService(AuthenticationService authenticationService)
    {
        this.authenticationService = authenticationService;
    }

    public AuthenticationService getAuthenticationService()
    {
        return authenticationService;
    }

    /**
     * Sets the permission service.
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }

    /**
     * Sets the permission model DAO.
     */
    public void setPermissionModelDao(ModelDAO permissionModelDao)
    {
        this.permissionModelDao = permissionModelDao;
    }

    public void setOpenCMISDictionaryService(CMISDictionaryService cmisDictionaryService)
    {
        this.cmisDictionaryService = cmisDictionaryService;
    }

    public void setOpenCMISDictionaryService11(CMISDictionaryService cmisDictionaryService)
    {
        this.cmisDictionaryService11 = cmisDictionaryService;
    }

    public CMISDictionaryService getOpenCMISDictionaryService()
    {
        CmisVersion cmisVersion = getRequestCmisVersion();
        if (cmisVersion.equals(CmisVersion.CMIS_1_0))
        {
            return cmisDictionaryService;
        }
        else
        {
            return cmisDictionaryService11;
        }
    }

    /**
     * Sets the OpenCMIS query service.
     */
    public void setOpenCMISQueryService(CMISQueryService cmisQueryService)
    {
        this.cmisQueryService = cmisQueryService;
    }

    public void setOpenCMISQueryService11(CMISQueryService cmisQueryService)
    {
        this.cmisQueryService11 = cmisQueryService;
    }

    public CMISQueryService getOpenCMISQueryService()
    {
        CmisVersion cmisVersion = getRequestCmisVersion();
        if (cmisVersion.equals(CmisVersion.CMIS_1_0))
        {
            return cmisQueryService;
        }
        else
        {
            return cmisQueryService11;
        }
    }

    /**
     * Sets the MIME type service.
     */
    public void setMimetypeService(MimetypeService mimetypeService)
    {
        this.mimetypeService = mimetypeService;
    }

    public MimetypeService getMimetypeService()
    {
        return mimetypeService;
    }

    /**
     * Sets the audit service.
     */
    public void setAuditService(AuditService auditService)
    {
        this.auditService = auditService;
    }

    /**
     * Sets the namespace service.
     */
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }

    /**
     * Sets the search service.
     */
    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }

    public SearchService getSearchService()
    {
        return searchService;
    }

    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }

    public DictionaryService getDictionaryService()
    {
        return dictionaryService;
    }

    /**
     * Not implemented
     * 
     * @throws UnsupportedOperationException
     *             always
     */
    public void setProxyUser(String proxyUser)
    {
        // this.proxyUser = proxyUser;
        throw new UnsupportedOperationException("proxyUser setting not implemented.  Please raise a JIRA request.");
    }

    public String getProxyUser()
    {
        return proxyUser;
    }

    /**
     * Sets bulk update properties.
     */
    public void setBulkMaxItems(int size)
    {
        bulkMaxItems = size;
    }

    public int getBulkMaxItems()
    {
        return bulkMaxItems;
    }

    public void setBulkBatchSize(int size)
    {
        bulkBatchSize = size;
    }

    public int getBulkBatchSize()
    {
        return bulkBatchSize;
    }

    public void setBulkWorkerThreads(int threads)
    {
        bulkWorkerThreads = threads;
    }

    public int getBulkWorkerThreads()
    {
        return bulkWorkerThreads;
    }

    // --------------------------------------------------------------
    // Lifecycle methods
    // --------------------------------------------------------------

    private File tmp;

    public void setup()
    {
        File tempDir = TempFileProvider.getTempDir();
        this.tmp = new File(tempDir, "CMISAppend");
        if (!this.tmp.exists() && !this.tmp.mkdir())
        {
            throw new AlfrescoRuntimeException("Failed to create CMIS temporary directory");
        }
    }

    public void init()
    {
        // register as tenant deployer
        tenantAdminService.register(this);

        // set up and cache rendition mapping
        getRenditionMapping();

        // cache root node ref
        getRootNodeRef();

        // cache permission definitions and permission mappings
        repositoryPermissions = getRepositoryPermissions();
        permissionMappings = getPermissionMappings();
    }

    public void destroy()
    {
        singletonCache.remove(KEY_CMIS_ROOT_NODEREF);
        singletonCache.remove(KEY_CMIS_RENDITION_MAPPING_NODEREF);
    }

    public void onEnableTenant()
    {
        init();
    }

    public void onDisableTenant()
    {
        destroy();
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        lifecycle.setApplicationContext(applicationContext);
    }

    public void onApplicationEvent(ApplicationContextEvent event)
    {
        lifecycle.onApplicationEvent(event);
    }

    /**
     * Hooks into Spring Application Lifecycle.
     */
    private class ProcessorLifecycle extends AbstractLifecycleBean
    {
        @Override
        protected void onBootstrap(ApplicationEvent event)
        {
            init();
        }

        @Override
        protected void onShutdown(ApplicationEvent event)
        {}
    }

    // --------------------------------------------------------------
    // Alfresco methods
    // --------------------------------------------------------------

    /* For the given cmis property name get the corresponding Alfresco property name.
     *
     * Certain CMIS properties (e.g. cmis:creationDate and cmis:lastModifiedBy) don't have direct mappings to Alfresco properties through the CMIS dictionary, because these mappings should not be exposed outside the repository through CMIS. For these, however, this method provides the mapping so that the sort works. */
    public Pair<QName, Boolean> getSortProperty(String cmisPropertyName, String direction)
    {
        QName sortPropName = null;
        Pair<QName, Boolean> sortProp = null;

        PropertyDefinitionWrapper propDef = getOpenCMISDictionaryService().findPropertyByQueryName(cmisPropertyName);
        if (propDef != null)
        {
            if (propDef.getPropertyId().equals(PropertyIds.BASE_TYPE_ID))
            {
                // special-case (see also ALF-13968) - for getChildren, using "cmis:baseTypeId" allows sorting of folders first and vice-versa (cmis:folder <-> cmis:document)
                sortPropName = GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER;
            }
            else
            {
                sortPropName = propDef.getPropertyAccessor().getMappedProperty();
            }

            if (sortPropName == null)
            {
                // ok to map these properties because we are always getting current versions of nodes
                sortPropName = SORT_PROPERTY_MAPPINGS.get(cmisPropertyName);
            }
        }

        if (sortPropName != null)
        {
            boolean sortAsc = (direction == null ? true : direction.equalsIgnoreCase("asc"));
            sortProp = new Pair<QName, Boolean>(sortPropName, sortAsc);
        }
        else
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Ignore sort property '" + cmisPropertyName + " - mapping not found");
            }
        }

        return sortProp;
    }

    /**
     * Asynchronously generates thumbnails for the given node.
     *
     * @param nodeRef
     *            NodeRef
     */
    public void createThumbnails(NodeRef nodeRef, Set<String> thumbnailNames)
    {
        if (thumbnailNames == null || thumbnailNames.size() == 0)
        {
            return;
        }

        ThumbnailRegistry registry = thumbnailService.getThumbnailRegistry();

        // If there's nothing currently registered to generate thumbnails for the
        // specified mimetype, then log a message and bail out
        Serializable value = this.nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
        ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, value);
        if (contentData == null)
        {
            logger.info("Unable to create thumbnails as there is no content");
            return;
        }

        long size = contentData.getSize();
        String mimeType = contentData.getMimetype();

        for (String thumbnailName : thumbnailNames)
        {
            // Use the thumbnail registy to get the details of the thumbail
            ThumbnailDefinition details = registry.getThumbnailDefinition(thumbnailName);
            if (details == null)
            {
                // Throw exception
                logger.warn("The thumbnail name '" + thumbnailName + "' is not registered");
                continue;
            }
            else
            {
                if (registry.isThumbnailDefinitionAvailable(contentData.getContentUrl(), mimeType, size, nodeRef, details))
                {
                    org.alfresco.service.cmr.action.Action action = ThumbnailHelper.createCreateThumbnailAction(details, serviceRegistry);

                    // Queue async creation of thumbnail
                    actionService.executeAction(action, nodeRef, true, true);
                }
                else
                {
                    logger.info("Unable to create thumbnail '" + details.getName() + "' for " +
                            mimeType + " as no transformer is currently available");
                }
            }
        }
    }

    /**
     * Extracts metadata for the node.
     *
     * @param nodeRef
     *            NodeRef
     */
    public void extractMetadata(NodeRef nodeRef)
    {
        org.alfresco.service.cmr.action.Action action = actionService.createAction(ContentMetadataExtracter.EXECUTOR_NAME);
        action.setExecuteAsynchronously(true);
        actionService.executeAction(action, nodeRef, true, true);
    }

    public SiteInfo getSite(NodeRef nodeRef)
    {
        return siteService.getSite(nodeRef);
    }

    /**
     * Should the node be filtered?
     */
    public boolean filter(NodeRef nodeRef)
    {
        return objectFilter.filter(nodeRef);
    }

    public boolean disableBehaviour(QName className)
    {
        boolean wasEnabled = behaviourFilter.isEnabled(className);
        if (wasEnabled)
        {
            behaviourFilter.disableBehaviour(className);
        }
        return wasEnabled;
    }

    public boolean enableBehaviour(QName className)
    {
        boolean isEnabled = behaviourFilter.isEnabled(className);
        if (!isEnabled)
        {
            behaviourFilter.enableBehaviour(className);
        }
        return isEnabled;
    }

    public boolean disableBehaviour(QName className, NodeRef nodeRef)
    {
        boolean wasEnabled = behaviourFilter.isEnabled(nodeRef, className);
        if (wasEnabled)
        {
            behaviourFilter.disableBehaviour(nodeRef, className);
        }
        return wasEnabled;
    }

    public boolean enableBehaviour(QName className, NodeRef nodeRef)
    {
        boolean isEnabled = behaviourFilter.isEnabled(nodeRef, className);
        if (!isEnabled)
        {
            behaviourFilter.enableBehaviour(nodeRef, className);
        }
        return isEnabled;
    }

    public StoreRef getRootStoreRef()
    {
        return getRootNodeRef().getStoreRef();
    }

    public void deleteNode(NodeRef nodeRef, boolean postActivity)
    {
        ActivityInfo activityInfo = null;

        // post activity after removal of the node
        postActivity &= !hiddenAspect.hasHiddenAspect(nodeRef);
        if (postActivity)
        {
            // get this information before the node is deleted
            activityInfo = activityPoster.getActivityInfo(nodeRef);
        }

        getNodeService().deleteNode(nodeRef);

        // post activity after removal of the node
        if (postActivity && activityInfo != null)
        {
            activityPoster.postFileFolderDeleted(activityInfo);
        }
    }

    public RetryingTransactionHelper getRetryingTransactionHelper()
    {
        return transactionService.getRetryingTransactionHelper();
    }

    /**
     * Returns the root folder node ref.
     */
    public NodeRef getRootNodeRef()
    {
        NodeRef rootNodeRef = (NodeRef) singletonCache.get(KEY_CMIS_ROOT_NODEREF);
        if (rootNodeRef == null)
        {
            rootNodeRef = AuthenticationUtil.runAs(new RunAsWork<NodeRef>() {
                public NodeRef doWork() throws Exception
                {
                    return transactionService.getRetryingTransactionHelper().doInTransaction(
                            new RetryingTransactionCallback<NodeRef>() {
                                public NodeRef execute() throws Exception
                                {
                                    NodeRef root = nodeService.getRootNode(storeRef);
                                    List<NodeRef> rootNodes = searchService.selectNodes(root, rootPath, null,
                                            namespaceService, false);
                                    if (rootNodes.size() != 1)
                                    {
                                        throw new CmisRuntimeException("Unable to locate CMIS root path " + rootPath);
                                    }
                                    return rootNodes.get(0);
                                };
                            }, true);
                }
            }, AuthenticationUtil.getSystemUserName());

            if (rootNodeRef == null)
            {
                throw new CmisObjectNotFoundException("Root folder path '" + rootPath + "' not found!");
            }

            singletonCache.put(KEY_CMIS_ROOT_NODEREF, rootNodeRef);
        }

        return rootNodeRef;
    }

    public String getName(NodeRef nodeRef)
    {
        try
        {
            Object name = nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
            return (name instanceof String ? (String) name : null);
        }
        catch (InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Cuts of the version information from an object id.
     */
    public String getCurrentVersionId(String objectId)
    {
        if (objectId == null)
        {
            return null;
        }

        int sepIndex = objectId.lastIndexOf(ID_SEPERATOR);
        if (sepIndex > -1)
        {
            return objectId.substring(0, sepIndex);
        }

        return objectId;
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(String objectId)
    {
        return new CMISNodeInfoImpl(this, objectId);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef)
    {
        return createNodeInfo(nodeRef, null, null, null, true);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(NodeRef nodeRef, QName nodeType, Map<QName, Serializable> nodeProps, VersionHistory versionHistory, boolean checkExists)
    {
        return new CMISNodeInfoImpl(this, nodeRef, nodeType, nodeProps, versionHistory, checkExists);
    }

    /**
     * Creates an object info object.
     */
    public CMISNodeInfoImpl createNodeInfo(AssociationRef assocRef)
    {
        return new CMISNodeInfoImpl(this, assocRef);
    }

    /* Strip store ref from the id, if there is one. */
    private String getGuid(String id)
    {
        int idx = id.lastIndexOf("/");
        if (idx != -1)
        {
            return id.substring(idx + 1);
        }
        else
        {
            return id;
        }
    }

    /* Construct an object id based on the incoming assocRef and versionLabel. The object id will always be the assocRef guid. */
    public String constructObjectId(AssociationRef assocRef, String versionLabel)
    {
        return constructObjectId(assocRef, versionLabel, isPublicApi());
    }

    public String constructObjectId(AssociationRef assocRef, String versionLabel, boolean dropStoreRef)
    {
        StringBuilder sb = new StringBuilder(CMISConnector.ASSOC_ID_PREFIX);
        if (dropStoreRef)
        {
            // always return the guid
            sb.append(assocRef.getId());
        }
        else
        {
            sb.append(assocRef.toString());
        }

        if (versionLabel != null)
        {
            sb.append(CMISConnector.ID_SEPERATOR);
            sb.append(versionLabel);
        }
        return sb.toString();
    }

    /* Construct an object id based on the incoming incomingObjectId. The object id will always be the node guid. */
    public String constructObjectId(String incomingObjectId)
    {
        return constructObjectId(incomingObjectId, null);
    }

    /* Construct an object id based on the incoming incomingNodeId and versionLabel. The object id will always be the node guid. */
    public String constructObjectId(String incomingNodeId, String versionLabel)
    {
        return constructObjectId(incomingNodeId, versionLabel, isPublicApi());
    }

    public String constructObjectId(String incomingNodeId, String versionLabel, boolean dropStoreRef)
    {
        StringBuilder sb = new StringBuilder();
        if (dropStoreRef)
        {
            // always return the guid
            sb.append(getGuid(incomingNodeId));
        }
        else
        {
            sb.append(incomingNodeId);
        }
        if (versionLabel != null)
        {
            sb.append(CMISConnector.ID_SEPERATOR);
            sb.append(versionLabel);
        }
        return sb.toString();
    }

    public void createVersion(NodeRef nodeRef, VersionType versionType, String reason)
    {
        if (versionService.getVersionHistory(nodeRef) == null)
        {
            // no version history. Make sure we have an initial major version 1.0.
            Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(2);
            versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR);
            versionProperties.put(VersionModel.PROP_DESCRIPTION, "Initial version");
            versionService.createVersion(nodeRef, versionProperties);
        }

        Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(2);
        versionProperties.put(VersionModel.PROP_VERSION_TYPE, versionType);
        versionProperties.put(VersionModel.PROP_DESCRIPTION, reason);
        versionService.createVersion(nodeRef, versionProperties);
    }

    public CmisVersion getRequestCmisVersion()
    {
        CallContext callContext = AlfrescoCmisServiceCall.get();
        CmisVersion cmisVersion = (callContext != null ? callContext.getCmisVersion() : CmisVersion.CMIS_1_0);
        return cmisVersion;
    }

    private boolean isPublicApi()
    {
        boolean isPublicApi = false;
        CallContext callContext = AlfrescoCmisServiceCall.get();
        if (callContext != null)
        {
            String value = (String) callContext.get("isPublicApi");
            isPublicApi = (value == null ? false : Boolean.parseBoolean(value));
        }
        return isPublicApi;
    }

    /* Construct an object id based on the incoming incomingNodeRef and versionLabel. The object id will always be the incomingNodeRef guid. */
    public String constructObjectId(NodeRef incomingNodeRef, String versionLabel)
    {
        return constructObjectId(incomingNodeRef, versionLabel, isPublicApi());
    }

    public String constructObjectId(NodeRef incomingNodeRef, String versionLabel, boolean dropStoreRef)
    {
        StringBuilder sb = new StringBuilder();
        sb.append(dropStoreRef ? incomingNodeRef.getId() : incomingNodeRef.toString());
        if (versionLabel != null)
        {
            sb.append(CMISConnector.ID_SEPERATOR);
            sb.append(versionLabel);
        }
        return sb.toString();
    }

    /**
     * Compiles a CMIS object if for a live node.
     */
    public String createObjectId(NodeRef nodeRef)
    {
        return createObjectId(nodeRef, isPublicApi());
    }

    public String createObjectId(NodeRef nodeRef, boolean dropStoreRef)
    {
        QName typeQName = nodeService.getType(nodeRef);
        TypeDefinitionWrapper type = getOpenCMISDictionaryService().findNodeType(typeQName);

        if (type instanceof ItemTypeDefinitionWrapper)
        {
            return constructObjectId(nodeRef, null);
        }

        if (type instanceof FolderTypeDefintionWrapper)
        {
            return constructObjectId(nodeRef, null, dropStoreRef);
        }

        Serializable versionLabel = getNodeService()
                .getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL);
        if (versionLabel == null)
        {
            versionLabel = CMISConnector.UNVERSIONED_VERSION_LABEL;
        }

        return constructObjectId(nodeRef, (String) versionLabel, dropStoreRef);
    }

    private boolean isFolder(NodeRef nodeRef)
    {
        return getType(nodeRef) instanceof FolderTypeDefintionWrapper;
    }

    /**
     * Returns the type definition of a node or <code>null</code> if no type definition could be found.
     */
    public TypeDefinitionWrapper getType(NodeRef nodeRef)
    {
        try
        {
            QName typeQName = nodeService.getType(nodeRef);
            return getOpenCMISDictionaryService().findNodeType(typeQName);
        }
        catch (InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Returns the type definition of an association or <code>null</code> if no type definition could be found.
     */
    public TypeDefinitionWrapper getType(AssociationRef assocRef)
    {
        QName typeQName = assocRef.getTypeQName();
        return getOpenCMISDictionaryService().findAssocType(typeQName);
    }

    /**
     * Returns the type definition of a node or <code>null</code> if no type definition could be found.
     */
    public TypeDefinitionWrapper getType(String cmisTypeId)
    {
        return getOpenCMISDictionaryService().findType(cmisTypeId);
    }

    /**
     * Returns the definition after it has checked if the type can be used for object creation.
     */
    public TypeDefinitionWrapper getTypeForCreate(String cmisTypeId, BaseTypeId baseTypeId)
    {
        TypeDefinitionWrapper type = getType(cmisTypeId);
        if ((type == null) || (type.getBaseTypeId() != baseTypeId))
        {
            switch (baseTypeId)
            {
            case CMIS_DOCUMENT:
                throw new CmisConstraintException("Type is not a document type!");
            case CMIS_FOLDER:
                throw new CmisConstraintException("Type is not a folder type!");
            case CMIS_RELATIONSHIP:
                throw new CmisConstraintException("Type is not a relationship type!");
            case CMIS_POLICY:
                throw new CmisConstraintException("Type is not a policy type!");
            case CMIS_ITEM:
                throw new CmisConstraintException("Type is not an item type!");
            }
        }

        if (!type.getTypeDefinition(false).isCreatable())
        {
            throw new CmisConstraintException("Type is not creatable!");
        }

        return type;
    }

    /**
     * Applies a versioning state to a document.
     */
    public void applyVersioningState(NodeRef nodeRef, VersioningState versioningState)
    {
        if (versioningState == VersioningState.CHECKEDOUT)
        {
            if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE))
            {
                nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null);
            }

            // MNT-14687 : Creating a document as checkedout and then cancelling the checkout should delete the document
            nodeService.addAspect(nodeRef, ContentModel.ASPECT_CMIS_CREATED_CHECKEDOUT, null);

            getCheckOutCheckInService().checkout(nodeRef);
        }
        else if ((versioningState == VersioningState.MAJOR) || (versioningState == VersioningState.MINOR))
        {
            if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE))
            {
                nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null);
            }

            Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(5);
            versionProperties.put(
                    VersionModel.PROP_VERSION_TYPE,
                    versioningState == VersioningState.MAJOR ? VersionType.MAJOR : VersionType.MINOR);
            versionProperties.put(VersionModel.PROP_DESCRIPTION, "Initial Version");

            versionService.createVersion(nodeRef, versionProperties);
        }
    }

    /**
     * Checks if a child of a given type can be added to a given folder.
     */
    @SuppressWarnings("unchecked")
    public void checkChildObjectType(CMISNodeInfo folderInfo, String childType)
    {
        TypeDefinitionWrapper targetType = folderInfo.getType();
        PropertyDefinitionWrapper allowableChildObjectTypeProperty = targetType
                .getPropertyById(PropertyIds.ALLOWED_CHILD_OBJECT_TYPE_IDS);
        List<String> childTypes = (List<String>) allowableChildObjectTypeProperty.getPropertyAccessor().getValue(
                folderInfo);

        if ((childTypes == null) || childTypes.isEmpty())
        {
            return;
        }

        if (!childTypes.contains(childType))
        {
            throw new CmisConstraintException("Objects of type '" + childType + "' cannot be added to this folder!");
        }
    }

    /**
     * Creates the CMIS object for a node.
     */
    public ObjectData createCMISObject(CMISNodeInfo info, String filter, boolean includeAllowableActions,
            IncludeRelationships includeRelationships, String renditionFilter, boolean includePolicyIds,
            boolean includeAcl)
    {
        if (info.getType() == null)
        {
            throw new CmisObjectNotFoundException("No corresponding type found! Not a CMIS object?");
        }

        Properties nodeProps = (info.isRelationship() ? getAssocProperties(info, filter) : getNodeProperties(info, filter));

        return createCMISObjectImpl(info, nodeProps, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePolicyIds, includeAcl);
    }

    @SuppressWarnings("unchecked")
    private ObjectData createCMISObjectImpl(final CMISNodeInfo info, Properties nodeProps, String filter,
            boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            boolean includePolicyIds, boolean includeAcl)
    {
        final ObjectDataImpl result = new ObjectDataImpl();

        // set allowable actions
        if (includeAllowableActions)
        {
            result.setAllowableActions(getAllowableActions(info));
        }

        // set policy ids
        if (includePolicyIds)
        {
            result.setPolicyIds(new PolicyIdListImpl());
        }

        if (info.isRelationship())
        {
            // set properties
            result.setProperties(getAssocProperties(info, filter));

            // set ACL
            if (includeAcl)
            {
                // association have no ACL - return an empty list of ACEs
                result.setAcl(new AccessControlListImpl((List<Ace>) Collections.EMPTY_LIST));
                result.setIsExactAcl(Boolean.FALSE);
            }
        }
        else
        {
            // set properties
            result.setProperties(nodeProps);

            // set relationships
            if (includeRelationships != IncludeRelationships.NONE)
            {
                result.setRelationships(getRelationships(info.getNodeRef(), includeRelationships));
            }

            // set renditions
            if (!RENDITION_NONE.equals(renditionFilter))
            {
                List<RenditionData> renditions = getRenditions(info.getNodeRef(), renditionFilter, null, null);
                if ((renditions != null) && (!renditions.isEmpty()))
                {
                    result.setRenditions(renditions);
                }
                else
                {
                    result.setRenditions(Collections.EMPTY_LIST);
                }
            }

            // set ACL
            if (includeAcl)
            {
                AuthenticationUtil.runAsSystem(new RunAsWork<Void>() {
                    @Override
                    public Void doWork() throws Exception
                    {
                        Acl acl = getACL(info.getCurrentNodeNodeRef(), false);
                        if (acl != null)
                        {
                            result.setAcl(acl);
                            result.setIsExactAcl(acl.isExact());
                        }
                        return null;
                    }
                });
            }

            // add aspects
            List<CmisExtensionElement> extensions = getAspectExtensions(info, filter, result.getProperties()
                    .getProperties().keySet());

            if (!extensions.isEmpty())
            {
                result.getProperties().setExtensions(
                        Collections.singletonList((CmisExtensionElement) new CmisExtensionElementImpl(
                                ALFRESCO_EXTENSION_NAMESPACE, ASPECTS, null, extensions)));
            }
        }
        return result;
    }

    public String getPath(NodeRef nodeRef)
    {
        try
        {
            Path path = nodeService.getPath(nodeRef);

            // skip to CMIS root path
            NodeRef rootNode = getRootNodeRef();
            int i = 0;
            while (i < path.size())
            {
                Path.Element element = path.get(i);
                if (element instanceof ChildAssocElement)
                {
                    ChildAssociationRef assocRef = ((ChildAssocElement) element).getRef();
                    NodeRef node = assocRef.getChildRef();
                    if (node.equals(rootNode))
                    {
                        break;
                    }
                }
                i++;
            }

            StringBuilder displayPath = new StringBuilder(64);

            if (path.size() - i == 1)
            {
                // render root path
                displayPath.append("/");
            }
            else
            {
                // render CMIS scoped path
                i++;
                while (i < path.size())
                {
                    Path.Element element = path.get(i);
                    if (element instanceof ChildAssocElement)
                    {
                        ChildAssociationRef assocRef = ((ChildAssocElement) element).getRef();
                        NodeRef node = assocRef.getChildRef();
                        displayPath.append("/");
                        displayPath.append(nodeService.getProperty(node, ContentModel.PROP_NAME));
                    }
                    i++;
                }
            }

            return displayPath.toString();
        }
        catch (InvalidNodeRefException inre)
        {
            return null;
        }
    }

    /**
     * Gets the content from the repository.
     */
    public ContentStream getContentStream(CMISNodeInfo info, String streamId, BigInteger offset, BigInteger length)
    {
        // get the type and check if the object can have content
        TypeDefinitionWrapper type = info.getType();
        checkDocumentTypeForContent(type);

        // looks like a document, now get the content
        ContentStreamImpl result = new ContentStreamImpl();
        result.setFileName(info.getName());

        // if streamId is set, fetch other content
        NodeRef streamNodeRef = info.getNodeRef();
        if ((streamId != null) && (streamId.length() > 0))
        {
            CMISNodeInfo streamInfo = createNodeInfo(streamId);
            if (!streamInfo.isVariant(CMISObjectVariant.CURRENT_VERSION))
            {
                throw new CmisInvalidArgumentException("Stream id is invalid: " + streamId + ", expected variant " + CMISObjectVariant.CURRENT_VERSION + ", got variant " + streamInfo.getObjectVariant());
            }

            streamNodeRef = streamInfo.getNodeRef();
            type = streamInfo.getType();
            checkDocumentTypeForContent(type);
        }

        // get the stream now
        try
        {
            ContentReader contentReader = contentService.getReader(streamNodeRef, ContentModel.PROP_CONTENT);
            if (contentReader == null)
            {
                throw new CmisConstraintException("Document has no content!");
            }

            result.setMimeType(contentReader.getMimetype());
            long contentSize = contentReader.getSize();

            if ((offset == null) && (length == null))
            {
                result.setStream(contentReader.getContentInputStream());
                result.setLength(BigInteger.valueOf(contentSize));
                publishReadEvent(streamNodeRef, info.getName(), result.getMimeType(), contentSize, contentReader.getEncoding(), null);
            }
            else
            {
                long off = (offset == null ? 0 : offset.longValue());
                long len = (length == null ? contentSize : length.longValue());
                if (off + len > contentSize)
                {
                    len = contentReader.getSize() - off;
                }

                result.setStream(new RangeInputStream(contentReader.getContentInputStream(), off, len));
                result.setLength(BigInteger.valueOf(len));
                publishReadEvent(streamNodeRef, info.getName(), result.getMimeType(), contentSize, contentReader.getEncoding(), off + " - " + len);
            }
        }
        catch (Exception e)
        {
            if (e instanceof CmisBaseException)
            {
                throw (CmisBaseException) e;
            }
            else
            {
                StringBuilder msg = new StringBuilder("Failed to retrieve content: " + e.getMessage());
                Throwable cause = e.getCause();
                if (cause != null)
                {
                    // add the cause to the CMIS exception
                    msg.append(", ");
                    msg.append(cause.getMessage());
                }
                throw new CmisRuntimeException(msg.toString(), e);
            }
        }

        return result;
    }

    /**
     * Notifies listeners that a read has taken place.
     *
     * @param nodeRef
     *            NodeRef
     * @param name
     *            String
     * @param mimeType
     *            String
     * @param contentSize
     *            long
     * @param encoding
     *            String
     * @param range
     *            String
     */
    protected void publishReadEvent(final NodeRef nodeRef, final String name, final String mimeType, final long contentSize, final String encoding, final String range)
    {
        final QName nodeType = nodeRef == null ? null : nodeService.getType(nodeRef);

        eventPublisher.publishEvent(new EventPreparator() {
            @Override
            public Event prepareEvent(String user, String networkId, String transactionId)
            {
                if (StringUtils.hasText(range))
                {
                    return new ContentReadRangeEvent(user, networkId, transactionId,
                            nodeRef.getId(), null, nodeType.toString(), Client.asType(ClientType.cmis), name, mimeType, contentSize, encoding, range);
                }
                else
                {
                    return new ContentEventImpl(ContentEvent.DOWNLOAD, user, networkId, transactionId,
                            nodeRef.getId(), null, nodeType.toString(), Client.asType(ClientType.cmis), name, mimeType, contentSize, encoding);
                }
            }
        });
    }

    public void appendContent(CMISNodeInfo nodeInfo, ContentStream contentStream, boolean isLastChunk) throws IOException
    {
        NodeRef nodeRef = nodeInfo.getNodeRef();

        this.disableBehaviour(ContentModel.ASPECT_VERSIONABLE, nodeRef);

        if (!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT))
        {
            Map<QName, Serializable> props = new HashMap<QName, Serializable>();
            props.put(ContentModel.PROP_GOT_FIRST_CHUNK, true);
            nodeService.addAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT, props);
        }

        ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        InputStream existingContentInput = (reader != null ? reader.getContentInputStream() : null);
        InputStream input = contentStream.getStream();

        ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
        OutputStream out = new BufferedOutputStream(writer.getContentOutputStream());

        InputStream in = null;
        if (existingContentInput != null)
        {
            in = new SequenceInputStream(existingContentInput, input);
        }
        else
        {
            in = input;
        }

        try
        {
            IOUtils.copy(in, out);

            if (isLastChunk)
            {
                nodeService.removeAspect(nodeRef, ContentModel.ASPECT_CMIS_UPDATE_CONTEXT);
                getActivityPoster().postFileFolderUpdated(nodeInfo.isFolder(), nodeRef);

                createVersion(nodeRef, VersionType.MINOR, "Appended content stream");
            }
        }
        finally
        {
            if (in != null)
            {
                in.close();
            }
            if (out != null)
            {
                out.close();
            }

            this.enableBehaviour(ContentModel.ASPECT_VERSIONABLE, nodeRef);
        }
    }

    private void checkDocumentTypeForContent(TypeDefinitionWrapper type)
    {
        if (type == null)
        {
            throw new CmisObjectNotFoundException("No corresponding type found! Not a CMIS object?");
        }
        if (!(type instanceof DocumentTypeDefinitionWrapper))
        {
            throw new CmisStreamNotSupportedException("Object is not a document!");
        }
        if (((DocumentTypeDefinition) type.getTypeDefinition(false)).getContentStreamAllowed() == ContentStreamAllowed.NOTALLOWED)
        {
            throw new CmisConstraintException("Document cannot have content!");
        }
    }

    private void addAspectProperties(CMISNodeInfo info, String filter, PropertiesImpl result)
    {
        if (getRequestCmisVersion().equals(CmisVersion.CMIS_1_1))
        {
            Set<String> propertyIds = new HashSet<>();
            Set<String> filterSet = splitFilter(filter);

            Set<QName> aspects = info.getNodeAspects();
            for (QName aspect : aspects)
            {
                TypeDefinitionWrapper aspectType = getOpenCMISDictionaryService().findNodeType(aspect);
                if (aspectType == null)
                {
                    continue;
                }

                for (PropertyDefinitionWrapper propDef : aspectType.getProperties())
                {
                    if (propertyIds.contains(propDef.getPropertyId()))
                    {
                        // skip properties that have already been added
                        continue;
                    }

                    if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                    {
                        // skip properties that are not in the filter
                        continue;
                    }

                    Serializable value = propDef.getPropertyAccessor().getValue(info);
                    result.addProperty(getProperty(propDef.getPropertyDefinition().getPropertyType(), propDef, value));

                    // mark property as 'added'
                    propertyIds.add(propDef.getPropertyId());
                }
            }
        }
    }

    public Properties getNodeProperties(CMISNodeInfo info, String filter)
    {
        PropertiesImpl result = new PropertiesImpl();

        Set<String> filterSet = splitFilter(filter);

        for (PropertyDefinitionWrapper propDef : info.getType().getProperties())
        {
            if (!propDef.getPropertyId().equals(PropertyIds.OBJECT_ID))
            {
                // don't filter the object id
                if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                {
                    // skip properties that are not in the filter
                    continue;
                }
            }

            Serializable value = propDef.getPropertyAccessor().getValue(info);
            result.addProperty(getProperty(propDef.getPropertyDefinition().getPropertyType(), propDef, value));
        }

        addAspectProperties(info, filter, result);

        return result;
    }

    public Properties getAssocProperties(CMISNodeInfo info, String filter)
    {
        PropertiesImpl result = new PropertiesImpl();

        Set<String> filterSet = splitFilter(filter);

        for (PropertyDefinitionWrapper propDefWrap : info.getType().getProperties())
        {
            PropertyDefinition<?> propDef = propDefWrap.getPropertyDefinition();
            if ((filterSet != null) && (!filterSet.contains(propDef.getQueryName())))
            {
                // skip properties that are not in the filter
                continue;
            }

            CMISPropertyAccessor cmisPropertyAccessor = propDefWrap.getPropertyAccessor();
            Serializable value = cmisPropertyAccessor.getValue(info);
            PropertyType propType = propDef.getPropertyType();
            PropertyData<?> propertyData = getProperty(propType, propDefWrap, value);
            result.addProperty(propertyData);
        }

        return result;
    }

    /**
     * Builds aspect extension.
     */
    public List<CmisExtensionElement> getAspectExtensions(CMISNodeInfo info, String filter, Set<String> alreadySetProperties)
    {
        List<CmisExtensionElement> extensions = new ArrayList<CmisExtensionElement>();
        Set<String> propertyIds = new HashSet<String>(alreadySetProperties);
        List<CmisExtensionElement> propertyExtensionList = new ArrayList<CmisExtensionElement>();
        Set<String> filterSet = splitFilter(filter);

        Set<QName> aspects = info.getNodeAspects();
        for (QName aspect : aspects)
        {
            TypeDefinitionWrapper aspectType = getOpenCMISDictionaryService().findNodeType(aspect);
            if (aspectType == null)
            {
                continue;
            }

            AspectDefinition aspectDefinition = dictionaryService.getAspect(aspect);
            Map<QName, org.alfresco.service.cmr.dictionary.PropertyDefinition> aspectProperties = aspectDefinition.getProperties();

            extensions.add(new CmisExtensionElementImpl(ALFRESCO_EXTENSION_NAMESPACE, APPLIED_ASPECTS, null, aspectType
                    .getTypeId()));

            for (PropertyDefinitionWrapper propDef : aspectType.getProperties())
            {
                boolean addPropertyToExtensionList = getRequestCmisVersion().equals(CmisVersion.CMIS_1_1) && aspectProperties.keySet().contains(propDef.getAlfrescoName());
                // MNT-11876 : add property to extension even if it has been returned (CMIS 1.1)
                if (propertyIds.contains(propDef.getPropertyId()) && !addPropertyToExtensionList)
                {
                    // skip properties that have already been added
                    continue;
                }

                if ((filterSet != null) && (!filterSet.contains(propDef.getPropertyDefinition().getQueryName())))
                {
                    // skip properties that are not in the filter
                    continue;
                }

                Serializable value = propDef.getPropertyAccessor().getValue(info);
                propertyExtensionList.add(createAspectPropertyExtension(propDef.getPropertyDefinition(), value));

                // mark property as 'added'
                propertyIds.add(propDef.getPropertyId());
            }
        }

        if (!propertyExtensionList.isEmpty())
        {
            CmisExtensionElementImpl propertiesExtension = new CmisExtensionElementImpl(
                    ALFRESCO_EXTENSION_NAMESPACE, "properties", null, propertyExtensionList);
            extensions.addAll(Collections.singletonList(propertiesExtension));
        }

        return extensions;
    }

    /**
     * Creates a property extension element.
     */
    @SuppressWarnings("rawtypes")
    private CmisExtensionElement createAspectPropertyExtension(PropertyDefinition<?> propertyDefinition, Object value)
    {
        String name;
        switch (propertyDefinition.getPropertyType())
        {
        case BOOLEAN:
            name = "propertyBoolean";
            break;
        case DATETIME:
            name = "propertyDateTime";
            break;
        case DECIMAL:
            name = "propertyDecimal";
            break;
        case INTEGER:
            name = "propertyInteger";
            break;
        case ID:
            name = "propertyId";
            break;
        default:
            name = "propertyString";
        }

        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put("propertyDefinitionId", propertyDefinition.getId());
        attributes.put("queryName", propertyDefinition.getQueryName());
        // optional value
        if (propertyDefinition.getDisplayName() != null && propertyDefinition.getDisplayName().trim().length() > 0)
        {
            attributes.put("displayName", propertyDefinition.getDisplayName());
        }
        // optional value
        if (propertyDefinition.getLocalName() != null && propertyDefinition.getLocalName().trim().length() > 0)
        {
            attributes.put("localName", propertyDefinition.getLocalName());
        }

        List<CmisExtensionElement> propertyValues = new ArrayList<CmisExtensionElement>();
        if (value != null)
        {
            if (value instanceof List)
            {
                for (Object o : ((List) value))
                {
                    if (o != null)
                    {
                        propertyValues.add(new CmisExtensionElementImpl(CMIS_NAMESPACE, "value", null,
                                convertAspectPropertyValue(o)));
                    }
                    else
                    {
                        logger.warn("Unexpected null entry in list value for property " + propertyDefinition.getDisplayName()
                                + ", value = " + value);
                    }
                }
            }
            else
            {
                propertyValues.add(new CmisExtensionElementImpl(CMIS_NAMESPACE, "value", null,
                        convertAspectPropertyValue(value)));
            }
        }

        return new CmisExtensionElementImpl(CMIS_NAMESPACE, name, attributes, propertyValues);
    }

    private String convertAspectPropertyValue(Object value)
    {
        if (value instanceof Date)
        {
            GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
            cal.setTime((Date) value);
            value = cal;
        }

        if (value instanceof GregorianCalendar)
        {
            DatatypeFactory df;
            try
            {
                df = DatatypeFactory.newInstance();
            }
            catch (DatatypeConfigurationException e)
            {
                throw new IllegalArgumentException("Aspect conversation exception: " + e.getMessage(), e);
            }
            return df.newXMLGregorianCalendar((GregorianCalendar) value).toXMLFormat();
        }

        // MNT-12496 MNT-15044
        // Filter for AtomPub and Web services bindings only. Browser/json binding already encodes.
        if (AlfrescoCmisServiceCall.get() != null &&
                (CallContext.BINDING_ATOMPUB.equals(AlfrescoCmisServiceCall.get().getBinding()) ||
                        CallContext.BINDING_WEBSERVICES.equals(AlfrescoCmisServiceCall.get().getBinding())))
        {
            return filterXmlRestrictedCharacters(value.toString());
        }
        else
        {
            return value.toString();
        }
    }

    @SuppressWarnings("unchecked")
    private AbstractPropertyData<?> getProperty(PropertyType propType, PropertyDefinitionWrapper propDef,
            Serializable value)
    {
        AbstractPropertyData<?> result = null;
        switch (propType)
        {
        case BOOLEAN:
            result = new PropertyBooleanImpl();
            if (value instanceof List)
            {
                ((PropertyBooleanImpl) result).setValues((List<Boolean>) value);
            }
            else
            {
                ((PropertyBooleanImpl) result).setValue((Boolean) value);
            }
            break;
        case DATETIME:
            result = new PropertyDateTimeImpl();
            if (value instanceof List)
            {
                ((PropertyDateTimeImpl) result).setValues((List<GregorianCalendar>) DefaultTypeConverter.INSTANCE
                        .convert(GregorianCalendar.class, (List<?>) value));
            }
            else
            {
                ((PropertyDateTimeImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(GregorianCalendar.class,
                        value));
            }
            break;
        case DECIMAL:
            result = new PropertyDecimalImpl();
            if (value instanceof List)
            {
                ((PropertyDecimalImpl) result).setValues((List<BigDecimal>) DefaultTypeConverter.INSTANCE.convert(
                        BigDecimal.class, (List<?>) value));
            }
            else
            {
                ((PropertyDecimalImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(BigDecimal.class, value));
            }
            break;
        case HTML:
            result = new PropertyHtmlImpl();
            if (value instanceof List)
            {
                ((PropertyHtmlImpl) result).setValues((List<String>) value);
            }
            else
            {
                ((PropertyHtmlImpl) result).setValue((String) value);
            }
            break;
        case ID:
            result = new PropertyIdImpl();
            if (value instanceof List)
            {
                ((PropertyIdImpl) result).setValues((List<String>) value);
            }
            else
            {
                if (value instanceof NodeRef)
                {
                    ((PropertyIdImpl) result).setValue(constructObjectId((NodeRef) value, null));
                }
                else
                {
                    ((PropertyIdImpl) result).setValue((String) value);
                }
            }
            break;
        case INTEGER:
            result = new PropertyIntegerImpl();
            if (value instanceof List)
            {
                ((PropertyIntegerImpl) result).setValues((List<BigInteger>) DefaultTypeConverter.INSTANCE.convert(
                        BigInteger.class, (List<?>) value));
            }
            else
            {
                ((PropertyIntegerImpl) result).setValue(DefaultTypeConverter.INSTANCE.convert(BigInteger.class, value));
            }
            break;
        case STRING:
            result = new PropertyStringImpl();
            if (value instanceof List)
            {
                ((PropertyStringImpl) result).setValues((List<String>) value);
            }
            else
            {
                // MNT-12496 MNT-15044
                // Filter for AtomPub and Web services bindings only. Browser/json binding already encodes.
                if (AlfrescoCmisServiceCall.get() != null &&
                        (CallContext.BINDING_ATOMPUB.equals(AlfrescoCmisServiceCall.get().getBinding()) ||
                                CallContext.BINDING_WEBSERVICES.equals(AlfrescoCmisServiceCall.get().getBinding())))
                {
                    ((PropertyStringImpl) result).setValue(filterXmlRestrictedCharacters((String) value));
                }
                else
                {
                    ((PropertyStringImpl) result).setValue((String) value);
                }
            }
            break;
        case URI:
            result = new PropertyUriImpl();
            if (value instanceof List)
            {
                ((PropertyUriImpl) result).setValues((List<String>) value);
            }
            else
            {
                ((PropertyUriImpl) result).setValue((String) value);
            }
            break;
        default:
            throw new RuntimeException("Unknown datatype! Spec change?");
        }

        if (propDef != null)
        {
            result.setId(propDef.getPropertyDefinition().getId());
            result.setQueryName(propDef.getPropertyDefinition().getQueryName());
            result.setDisplayName(propDef.getPropertyDefinition().getDisplayName());
            result.setLocalName(propDef.getPropertyDefinition().getLocalName());
        }

        return result;
    }

    private String filterXmlRestrictedCharacters(String origValue)
    {
        if (origValue == null)
        {
            return origValue;
        }
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < origValue.length(); i++)
        {
            char ch = origValue.charAt(i);
            boolean restricted = (ch < '\u0020') && !(ch == '\t' || ch == '\n' || ch == '\r');
            sb.append(restricted ? REPLACEMENT_CHAR : ch);
        }

        return sb.toString();
    }

    private Set<String> splitFilter(String filter)
    {
        if (filter == null)
        {
            return null;
        }

        if (filter.trim().length() == 0)
        {
            return null;
        }

        Set<String> result = new HashSet<String>();
        for (String s : filter.split(","))
        {
            s = s.trim();
            if (s.equals("*"))
            {
                return null;
            }
            else if (s.length() > 0)
            {
                result.add(s);
            }
        }

        // set a few base properties
        result.add(QUERY_NAME_OBJECT_ID);
        result.add(QUERY_NAME_OBJECT_TYPE_ID);
        result.add(QUERY_NAME_BASE_TYPE_ID);

        return result;
    }

    public AllowableActions getAllowableActions(CMISNodeInfo info)
    {
        AllowableActionsImpl result = new AllowableActionsImpl();
        Set<Action> allowableActions = new HashSet<Action>();
        result.setAllowableActions(allowableActions);

        for (CMISActionEvaluator evaluator : info.getType().getActionEvaluators().values())
        {
            if (evaluator.isAllowed(info))
            {
                allowableActions.add(evaluator.getAction());
            }
        }

        return result;
    }

    public List<ObjectData> getRelationships(NodeRef nodeRef, IncludeRelationships includeRelationships/* , CmisVersion cmisVersion */)
    {
        List<ObjectData> result = new ArrayList<ObjectData>();

        if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL))
        {
            // relationships from and to versions are not preserved
            return result;
        }

        // get relationships
        List<AssociationRef> assocs = new ArrayList<AssociationRef>();
        if (includeRelationships == IncludeRelationships.SOURCE || includeRelationships == IncludeRelationships.BOTH)
        {
            assocs.addAll(nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }
        if (includeRelationships == IncludeRelationships.TARGET || includeRelationships == IncludeRelationships.BOTH)
        {
            assocs.addAll(nodeService.getSourceAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }

        // filter relationships that not map the CMIS domain model
        for (AssociationRef assocRef : assocs)
        {
            TypeDefinitionWrapper assocTypeDef = getOpenCMISDictionaryService().findAssocType(assocRef.getTypeQName());
            if (assocTypeDef == null || getType(assocRef.getSourceRef()) == null
                    || getType(assocRef.getTargetRef()) == null)
            {
                continue;
            }

            try
            {
                result.add(createCMISObject(createNodeInfo(assocRef), null, false, IncludeRelationships.NONE,
                        RENDITION_NONE, false, false/* , cmisVersion */));
            }
            catch (AccessDeniedException e)
            {
                // PUBLICAPI-110
                // ok, just skip it
            }
            catch (CmisObjectNotFoundException e)
            {
                // ignore objects that have not been found (perhaps because their type is unknown to CMIS)
            }
            // TODO: Somewhere this has not been wrapped correctly
            catch (net.sf.acegisecurity.AccessDeniedException e)
            {
                // skip
            }
        }

        return result;
    }

    public ObjectList getObjectRelationships(NodeRef nodeRef, RelationshipDirection relationshipDirection,
            String typeId, String filter, Boolean includeAllowableActions, BigInteger maxItems, BigInteger skipCount)
    {
        ObjectListImpl result = new ObjectListImpl();
        result.setHasMoreItems(false);
        result.setNumItems(BigInteger.ZERO);
        result.setObjects(new ArrayList<ObjectData>());

        if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL))
        {
            // relationships from and to versions are not preserved
            return result;
        }

        // get relationships
        List<AssociationRef> assocs = new ArrayList<AssociationRef>();
        if (relationshipDirection == RelationshipDirection.SOURCE
                || relationshipDirection == RelationshipDirection.EITHER)
        {
            assocs.addAll(nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }
        if (relationshipDirection == RelationshipDirection.TARGET
                || relationshipDirection == RelationshipDirection.EITHER)
        {
            assocs.addAll(nodeService.getSourceAssocs(nodeRef, RegexQNamePattern.MATCH_ALL));
        }

        int skip = (skipCount == null ? 0 : skipCount.intValue());
        int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
        int counter = 0;
        boolean hasMore = false;

        if (max > 0)
        {
            // filter relationships that not map the CMIS domain model
            for (AssociationRef assocRef : assocs)
            {
                TypeDefinitionWrapper assocTypeDef = getOpenCMISDictionaryService().findAssocType(assocRef.getTypeQName());
                if (assocTypeDef == null || getType(assocRef.getSourceRef()) == null
                        || getType(assocRef.getTargetRef()) == null)
                {
                    continue;
                }

                if ((typeId != null) && !assocTypeDef.getTypeId().equals(typeId))
                {
                    continue;
                }

                counter++;

                if (skip > 0)
                {
                    skip--;
                    continue;
                }

                max--;
                if (max > 0)
                {
                    try
                    {
                        result.getObjects().add(
                                createCMISObject(createNodeInfo(assocRef), filter, includeAllowableActions,
                                        IncludeRelationships.NONE, RENDITION_NONE, false, false/* , cmisVersion */));
                    }
                    catch (CmisObjectNotFoundException e)
                    {
                        // ignore objects that have not been found (perhaps because their type is unknown to CMIS)
                    }
                }
                else
                {
                    hasMore = true;
                }
            }
        }

        result.setNumItems(BigInteger.valueOf(counter));
        result.setHasMoreItems(hasMore);

        return result;
    }

    public List<RenditionData> getRenditions(NodeRef nodeRef, String renditionFilter, BigInteger maxItems,
            BigInteger skipCount)
    {
        CMISRenditionMapping mapping = getRenditionMapping();
        return mapping.getRenditions(nodeRef, renditionFilter, maxItems, skipCount);
    }

    public Acl getACL(NodeRef nodeRef, boolean onlyBasicPermissions)
    {
        AccessControlListImpl result = new AccessControlListImpl();

        // get the permissions and sort them
        ArrayList<AccessPermission> ordered = new ArrayList<AccessPermission>(
                permissionService.getAllSetPermissions(nodeRef));
        Collections.sort(ordered, new AccessPermissionComparator());

        // remove denied permissions and create OpenCMIS objects
        Map<String, Map<Boolean, AccessControlEntryImpl>> aceMap = new HashMap<String, Map<Boolean, AccessControlEntryImpl>>();
        for (AccessPermission entry : ordered)
        {
            if (entry.getAccessStatus() == AccessStatus.ALLOWED)
            {
                // add allowed entries
                Map<Boolean, AccessControlEntryImpl> directAce = aceMap.get(entry.getAuthority());
                if (directAce == null)
                {
                    directAce = new HashMap<Boolean, AccessControlEntryImpl>();
                    aceMap.put(entry.getAuthority(), directAce);
                }

                AccessControlEntryImpl ace = directAce.get(entry.isSetDirectly());
                if (ace == null)
                {
                    ace = new AccessControlEntryImpl();
                    ace.setPrincipal(new AccessControlPrincipalDataImpl(entry.getAuthority()));
                    ace.setPermissions(new ArrayList<String>());
                    ace.setDirect(entry.isSetDirectly());
                    directAce.put(entry.isSetDirectly(), ace);
                }

                ace.getPermissions().add(entry.getPermission());
            }
            else if (entry.getAccessStatus() == AccessStatus.DENIED)
            {
                // remove denied entries
                Map<Boolean, AccessControlEntryImpl> directAce = aceMap.get(entry.getAuthority());
                if (directAce != null)
                {
                    for (AccessControlEntryImpl ace : directAce.values())
                    {
                        ace.getPermissions().remove(entry.getPermission());
                    }
                }
            }
        }

        // adjust permissions, add CMIS permissions and add ACEs to ACL
        List<Ace> aces = new ArrayList<Ace>();
        result.setAces(aces);
        for (Map<Boolean, AccessControlEntryImpl> bothAces : aceMap.values())
        {
            // get, translate and set direct ACE
            AccessControlEntryImpl directAce = bothAces.get(true);
            if ((directAce != null) && (!directAce.getPermissions().isEmpty()))
            {
                List<String> permissions = translatePermissionsToCMIS(directAce.getPermissions(), onlyBasicPermissions);
                if (permissions != null && !permissions.isEmpty())
                {
                    // tck doesn't like empty permissions list
                    directAce.setPermissions(permissions);
                    aces.add(directAce);
                }
            }

            // get, translate, remove duplicate permissions and set indirect ACE
            AccessControlEntryImpl indirectAce = bothAces.get(false);
            if ((indirectAce != null) && (!indirectAce.getPermissions().isEmpty()))
            {
                List<String> permissions = translatePermissionsToCMIS(indirectAce.getPermissions(), onlyBasicPermissions);
                indirectAce.setPermissions(permissions);

                // remove permissions that are already set in the direct ACE
                if ((directAce != null) && (!directAce.getPermissions().isEmpty()))
                {
                    indirectAce.getPermissions().removeAll(directAce.getPermissions());
                }

                if (!indirectAce.getPermissions().isEmpty())
                {
                    // tck doesn't like empty permissions list
                    aces.add(indirectAce);
                }
            }
        }

        result.setExact(!onlyBasicPermissions);

        return result;
    }

    private List<String> translatePermissionsToCMIS(List<String> permissions, boolean onlyBasicPermissions)
    {
        Set<String> result = new TreeSet<String>();

        for (String permission : permissions)
        {
            PermissionReference permissionReference = permissionModelDao.getPermissionReference(null, permission);

            if (onlyBasicPermissions)
            {

                // check for full permissions
                if (permissionModelDao.hasFull(permissionReference))
                {
                    result.add(BasicPermissions.ALL);
                }

                // check short forms
                Set<PermissionReference> longForms = permissionModelDao.getGranteePermissions(permissionReference);

                HashSet<String> shortForms = new HashSet<String>();
                for (PermissionReference longForm : longForms)
                {
                    shortForms.add(permissionModelDao.isUnique(longForm) ? longForm.getName() : longForm.toString());
                }

                for (String perm : shortForms)
                {
                    if (PermissionService.READ.equals(perm))
                    {
                        result.add(BasicPermissions.READ);
                    }
                    else if (PermissionService.WRITE.equals(perm))
                    {
                        result.add(BasicPermissions.WRITE);
                    }
                    else if (PermissionService.ALL_PERMISSIONS.equals(perm))
                    {
                        result.add(BasicPermissions.ALL);
                    }
                }

                // check the permission
                if (PermissionService.READ.equals(permission))
                {
                    result.add(BasicPermissions.READ);
                }
                else if (PermissionService.WRITE.equals(permission))
                {
                    result.add(BasicPermissions.WRITE);
                }
                else if (PermissionService.ALL_PERMISSIONS.equals(permission))
                {
                    result.add(BasicPermissions.ALL);
                }
            }
            else
            {
                // ACE-2224: only repository specific permissions should be reported
                if (permission.startsWith("{"))
                {
                    result.add(permission);
                }
                // do revert conversion for basic permissions that have exact matching
                else if (PermissionService.READ.equals(permission))
                {
                    result.add(BasicPermissions.READ);
                }
                else if (PermissionService.WRITE.equals(permission))
                {
                    result.add(BasicPermissions.WRITE);
                }
                else if (PermissionService.ALL_PERMISSIONS.equals(permission))
                {
                    result.add(BasicPermissions.ALL);
                }
                else
                {
                    result.add(permissionReference.toString());
                }
            }
        }

        return new ArrayList<String>(result);
    }

    public static class AccessPermissionComparator implements Comparator<AccessPermission>
    {
        public int compare(AccessPermission left, AccessPermission right)
        {
            if (left.getPosition() != right.getPosition())
            {
                return right.getPosition() - left.getPosition();
            }
            else
            {
                if (left.getAccessStatus() != right.getAccessStatus())
                {
                    return (left.getAccessStatus() == AccessStatus.DENIED) ? -1 : 1;
                }
                else
                {
                    int compare = left.getAuthority().compareTo(right.getAuthority());
                    if (compare != 0)
                    {
                        return compare;
                    }
                    else
                    {
                        return (left.getPermission().compareTo(right.getPermission()));
                    }

                }

            }
        }
    }

    /**
     * Applies the given ACLs.
     */
    public void applyACL(NodeRef nodeRef, TypeDefinitionWrapper type, Acl addAces, Acl removeAces)
    {
        boolean hasAdd = (addAces != null) && (addAces.getAces() != null) && !addAces.getAces().isEmpty();
        boolean hasRemove = (removeAces != null) && (removeAces.getAces() != null) && !removeAces.getAces().isEmpty();

        if (!hasAdd && !hasRemove)
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllableAcl())
        {
            throw new CmisConstraintException("Object is not ACL controllable!");
        }

        // remove permissions
        if (hasRemove)
        {
            Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);

            // get only direct ACE since only those can be removed
            Acl onlyDirectAcl = excludeInheritedAces(nodeRef, removeAces);

            for (Ace ace : onlyDirectAcl.getAces())
            {
                String principalId = ace.getPrincipalId();
                if (CMIS_USER.equals(principalId))
                {
                    principalId = AuthenticationUtil.getFullyAuthenticatedUser();
                }

                for (String permission : translatePermissionsFromCMIS(ace.getPermissions()))
                {

                    AccessPermission toCheck = new AccessPermissionImpl(permission, AccessStatus.ALLOWED, principalId,
                            0);
                    if (!permissions.contains(toCheck))
                    {
                        throw new CmisConstraintException("No matching ACE found to remove!");
                    }

                    permissionService.deletePermission(nodeRef, principalId, permission);
                }
            }
        }

        // add permissions
        if (hasAdd)
        {
            for (Ace ace : addAces.getAces())
            {
                String principalId = ace.getPrincipalId();
                if (CMIS_USER.equals(principalId))
                {
                    principalId = AuthenticationUtil.getFullyAuthenticatedUser();
                }

                for (String permission : translatePermissionsFromCMIS(ace.getPermissions()))
                {
                    permissionService.setPermission(nodeRef, principalId, permission, true);
                }
            }
        }
    }

    /**
     * Converts Acl to map and ignore the indirect ACEs
     *
     * @param acl
     *            Acl
     * @return Map
     */
    private Map<String, Set<String>> convertAclToMap(Acl acl)
    {
        Map<String, Set<String>> result = new HashMap<String, Set<String>>();

        if (acl == null || acl.getAces() == null)
        {
            return result;
        }

        for (Ace ace : acl.getAces())
        {
            // don't consider indirect ACEs - we can't change them
            if (!ace.isDirect())
            {
                // ignore
                continue;
            }

            // although a principal must not be null, check it
            if (ace.getPrincipal() == null || ace.getPrincipal().getId() == null)
            {
                // ignore
                continue;
            }

            Set<String> permissions = result.get(ace.getPrincipal().getId());
            if (permissions == null)
            {
                permissions = new HashSet<String>();
                result.put(ace.getPrincipal().getId(), permissions);
            }

            if (ace.getPermissions() != null)
            {
                permissions.addAll(ace.getPermissions());
            }
        }

        return result;
    }

    /**
     * Filter acl to ignore inherited ACEs
     *
     * @param nodeRef
     *            NodeRef
     * @param acl
     *            Acl
     * @return Acl
     */
    protected Acl excludeInheritedAces(NodeRef nodeRef, Acl acl)
    {

        List<Ace> newAces = new ArrayList<Ace>();
        Acl allACLs = getACL(nodeRef, false);

        Map<String, Set<String>> originalsAcls = convertAclToMap(allACLs);
        Map<String, Set<String>> newAcls = convertAclToMap(acl);

        // iterate through the original ACEs
        for (Map.Entry<String, Set<String>> ace : originalsAcls.entrySet())
        {

            // add permissions
            Set<String> addPermissions = newAcls.get(ace.getKey());
            if (addPermissions != null)
            {
                ace.getValue().addAll(addPermissions);
            }

            // create new ACE
            if (!ace.getValue().isEmpty())
            {
                newAces.add(new AccessControlEntryImpl(new AccessControlPrincipalDataImpl(ace
                        .getKey()), new ArrayList<String>(ace.getValue())));
            }
        }

        return new AccessControlListImpl(newAces);
    }

    /**
     * Sets the given ACL.
     */
    public void applyACL(NodeRef nodeRef, TypeDefinitionWrapper type, Acl aces)
    {
        boolean hasAces = (aces != null) && (aces.getAces() != null) && !aces.getAces().isEmpty();

        if (!hasAces && !permissionService.getInheritParentPermissions(nodeRef))
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllableAcl())
        {
            throw new CmisConstraintException("Object is not ACL controllable!");
        }

        // remove all permissions
        permissionService.deletePermissions(nodeRef);

        // set new permissions
        for (Ace ace : aces.getAces())
        {
            String principalId = ace.getPrincipalId();
            if (CMIS_USER.equals(principalId))
            {
                principalId = AuthenticationUtil.getFullyAuthenticatedUser();
            }

            List<String> permissions = translatePermissionsFromCMIS(ace.getPermissions());
            for (String permission : permissions)
            {
                permissionService.setPermission(nodeRef, principalId, permission, true);
            }
        }
    }

    private List<String> translatePermissionsFromCMIS(List<String> permissions)
    {
        List<String> result = new ArrayList<String>();

        if (permissions == null)
        {
            return result;
        }

        for (String permission : permissions)
        {
            if (permission == null)
            {
                throw new CmisConstraintException("Invalid null permission!");
            }

            if (BasicPermissions.READ.equals(permission))
            {
                result.add(PermissionService.READ);
            }
            else if (BasicPermissions.WRITE.equals(permission))
            {
                result.add(PermissionService.WRITE);
            }
            else if (BasicPermissions.ALL.equals(permission))
            {
                result.add(PermissionService.ALL_PERMISSIONS);
            }
            else if (!permission.startsWith("{"))
            {
                result.add(permission);
            }
            else
            {
                int sepIndex = permission.lastIndexOf('.');
                if (sepIndex == -1)
                {
                    result.add(permission);
                }
                else
                {
                    result.add(permission.substring(sepIndex + 1));
                }
            }
        }

        return result;
    }

    public void applyPolicies(NodeRef nodeRef, TypeDefinitionWrapper type, List<String> policies)
    {
        if ((policies == null) || (policies.isEmpty()))
        {
            return;
        }

        if (!type.getTypeDefinition(false).isControllablePolicy())
        {
            throw new CmisConstraintException("Object is not policy controllable!");
        }

        // nothing else to do...
    }

    @SuppressWarnings("unchecked")
    public ObjectList query(String statement, Boolean includeAllowableActions,
            IncludeRelationships includeRelationships, String renditionFilter, BigInteger maxItems, BigInteger skipCount/* , CmisVersion cmisVersion */)
    {
        // prepare results
        ObjectListImpl result = new ObjectListImpl();
        result.setObjects(new ArrayList<ObjectData>());

        // prepare query
        CMISQueryOptions options = new CMISQueryOptions(statement, getRootStoreRef());
        CmisVersion cmisVersion = getRequestCmisVersion();
        options.setCmisVersion(cmisVersion);
        options.setQueryMode(CMISQueryMode.CMS_WITH_ALFRESCO_EXTENSIONS);

        int skip = 0;
        if ((skipCount != null) && (skipCount.intValue() >= 0))
        {
            skip = skipCount.intValue();
            options.setSkipCount(skip);
        }

        if ((maxItems != null) && (maxItems.intValue() >= 0))
        {
            options.setMaxItems(maxItems.intValue());
        }

        boolean fetchObject = includeAllowableActions || (includeRelationships != IncludeRelationships.NONE)
                || (!RENDITION_NONE.equals(renditionFilter));

        // query
        CMISResultSet rs = getOpenCMISQueryService().query(options);
        try
        {
            CMISResultSetColumn[] columns = rs.getMetaData().getColumns();

            for (CMISResultSetRow row : rs)
            {
                NodeRef nodeRef = row.getNodeRef();

                if (!nodeService.exists(nodeRef) || filter(nodeRef))
                {
                    continue;
                }

                TypeDefinitionWrapper type = getType(nodeRef);
                if (type == null)
                {
                    continue;
                }

                ObjectDataImpl hit = new ObjectDataImpl();
                PropertiesImpl properties = new PropertiesImpl();
                hit.setProperties(properties);

                Map<String, Serializable> values = row.getValues();

                for (CMISResultSetColumn column : columns)
                {
                    AbstractPropertyData<?> property = getProperty(column.getCMISDataType(),
                            column.getCMISPropertyDefinition(), values.get(column.getName()));
                    property.setQueryName(column.getName());
                    properties.addProperty(property);
                }

                if (fetchObject)
                {
                    // set allowable actions
                    if (includeAllowableActions)
                    {
                        CMISNodeInfoImpl nodeInfo = createNodeInfo(nodeRef);
                        if (!nodeInfo.getObjectVariant().equals(CMISObjectVariant.NOT_EXISTING))
                        {
                            hit.setAllowableActions(getAllowableActions(nodeInfo));
                        }
                    }

                    // set relationships
                    if (includeRelationships != IncludeRelationships.NONE)
                    {
                        hit.setRelationships(getRelationships(nodeRef, includeRelationships/* , cmisVersion */));
                    }

                    // set renditions
                    if (!RENDITION_NONE.equals(renditionFilter))
                    {
                        List<RenditionData> renditions = getRenditions(nodeRef, renditionFilter, null, null);
                        if ((renditions != null) && (!renditions.isEmpty()))
                        {
                            hit.setRenditions(renditions);
                        }
                        else
                        {
                            hit.setRenditions(Collections.EMPTY_LIST);
                        }
                    }
                }

                result.getObjects().add(hit);
            }

            long numberFound = rs.getNumberFound();
            if (numberFound != -1)
            {
                result.setNumItems(BigInteger.valueOf(numberFound));
            }
            result.setHasMoreItems(rs.hasMore());

        }
        finally
        {
            rs.close();
        }

        return result;
    }

    /**
     * Sets property values.
     */
    @SuppressWarnings({"rawtypes"})
    public void setProperties(NodeRef nodeRef, TypeDefinitionWrapper type, Properties properties, String... exclude)
    {
        if (properties == null)
        {
            return;
        }

        Map<String, PropertyData<?>> incomingPropsMap = properties.getProperties();
        if (incomingPropsMap == null)
        {
            return;
        }

        // extract property data into an easier to use form
        Map<String, Pair<TypeDefinitionWrapper, Serializable>> propsMap = new HashMap<String, Pair<TypeDefinitionWrapper, Serializable>>();
        for (String propertyId : incomingPropsMap.keySet())
        {
            PropertyData<?> property = incomingPropsMap.get(propertyId);
            PropertyDefinitionWrapper propDef = type.getPropertyById(property.getId());
            if (propDef == null)
            {
                propDef = getOpenCMISDictionaryService().findProperty(propertyId);
                if (propDef == null)
                {
                    throw new CmisInvalidArgumentException("Property " + property.getId() + " is unknown!");
                }
            }

            Boolean isOnWorkingCopy = checkOutCheckInService.isWorkingCopy(nodeRef);
            Updatability updatability = propDef.getPropertyDefinition().getUpdatability();
            if (!isUpdatable(updatability, isOnWorkingCopy))
            {
                throw new CmisInvalidArgumentException("Property " + propertyId + " is read-only!");
            }

            TypeDefinitionWrapper propType = propDef.getOwningType();
            Serializable value = getValue(property, propDef.getPropertyDefinition().getCardinality() == Cardinality.MULTI);
            Pair<TypeDefinitionWrapper, Serializable> pair = new Pair<TypeDefinitionWrapper, Serializable>(propType, value);
            propsMap.put(propertyId, pair);
        }

        // Need to do deal with secondary types first
        Pair<TypeDefinitionWrapper, Serializable> pair = propsMap.get(PropertyIds.SECONDARY_OBJECT_TYPE_IDS);
        Serializable secondaryTypesProperty = (pair != null ? pair.getSecond() : null);
        if (secondaryTypesProperty != null)
        {
            if (!(secondaryTypesProperty instanceof List))
            {
                throw new CmisInvalidArgumentException("Secondary types must be a list!");
            }
            List secondaryTypes = (List) secondaryTypesProperty;
            if (secondaryTypes != null && secondaryTypes.size() > 0)
            {
                // add/remove secondary types/aspects
                processSecondaryTypes(nodeRef, secondaryTypes, propsMap);
            }
        }

        for (String propertyId : propsMap.keySet())
        {
            if (propertyId.equals(PropertyIds.SECONDARY_OBJECT_TYPE_IDS))
            {
                // already handled above
                continue;
            }

            pair = propsMap.get(propertyId);
            TypeDefinitionWrapper propType = pair.getFirst();
            Serializable value = pair.getSecond();
            if (Arrays.binarySearch(exclude, propertyId) < 0)
            {
                setProperty(nodeRef, propType, propertyId, value);
            }
        }

        List<CmisExtensionElement> extensions = properties.getExtensions();
        if (extensions != null)
        {
            boolean isNameChanging = properties.getProperties().containsKey(PropertyIds.NAME);

            for (CmisExtensionElement extension : extensions)
            {
                if (ALFRESCO_EXTENSION_NAMESPACE.equals(extension.getNamespace())
                        && SET_ASPECTS.equals(extension.getName()))
                {
                    setAspectProperties(nodeRef, isNameChanging, extension);
                    break;
                }
            }
        }
    }

    @SuppressWarnings("rawtypes")
    private void processSecondaryTypes(NodeRef nodeRef, List secondaryTypes, Map<String, Pair<TypeDefinitionWrapper, Serializable>> propsToAdd)
    {
        // diff existing aspects and secondaryTypes/aspects list
        Set<QName> existingAspects = nodeService.getAspects(nodeRef);
        Set<QName> secondaryTypeAspects = new HashSet<QName>();
        for (Object o : secondaryTypes)
        {
            String secondaryType = (String) o;

            TypeDefinitionWrapper wrapper = getOpenCMISDictionaryService().findType(secondaryType);
            if (wrapper != null)
            {
                QName aspectQName = wrapper.getAlfrescoName();
                secondaryTypeAspects.add(aspectQName);
            }
            else
            {
                throw new CmisInvalidArgumentException("Invalid secondary type id " + secondaryType);
            }
        }

        Set<QName> aspectsToIgnore = new HashSet<>();
        aspectsToIgnore.add(ContentModel.ASPECT_REFERENCEABLE);
        aspectsToIgnore.add(ContentModel.ASPECT_LOCALIZED);
        aspectsToIgnore.add(ContentModel.ASPECT_WORKING_COPY);

        Set<String> namespacesToIgnore = new HashSet<>(singletonList(NamespaceService.SYSTEM_MODEL_1_0_URI));

        // aspects to add == the list of secondary types - existing aspects - ignored aspects
        Set<QName> toAdd = new HashSet<QName>(secondaryTypeAspects);
        toAdd.removeAll(existingAspects);
        toAdd.removeAll(aspectsToIgnore);
        toAdd.removeIf(a -> namespacesToIgnore.contains(a.getNamespaceURI()));

        // aspects to remove == existing aspects - secondary types
        Set<QName> aspectsToRemove = new HashSet<QName>();
        aspectsToRemove.addAll(existingAspects);
        aspectsToRemove.removeAll(aspectsToIgnore);
        Iterator<QName> it = aspectsToRemove.iterator();
        while (it.hasNext())
        {
            QName aspectQName = it.next();
            TypeDefinitionWrapper w = getOpenCMISDictionaryService().findNodeType(aspectQName);
            if (w == null || secondaryTypeAspects.contains(aspectQName) || namespacesToIgnore.contains(aspectQName.getNamespaceURI()))
            {
                // the type is not exposed,
                // or is in the secondary types to set,
                // or is in the set of namespaces to ignore,
                // so remove it from the "to remove" set
                it.remove();
            }
        }

        // first, remove aspects
        for (QName aspectQName : aspectsToRemove)
        {
            nodeService.removeAspect(nodeRef, aspectQName);
        }

        // add aspects and properties
        for (QName aspectQName : toAdd)
        {
            nodeService.addAspect(nodeRef, aspectQName, null);
        }
    }

    public void addSecondaryTypes(NodeRef nodeRef, List<String> secondaryTypes)
    {
        if (secondaryTypes != null && secondaryTypes.size() > 0)
        {
            for (String secondaryType : secondaryTypes)
            {
                TypeDefinitionWrapper type = getType(secondaryType);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid secondaryType: " + secondaryType);
                }
                nodeService.addAspect(nodeRef, type.getAlfrescoName(), null);
            }
        }
    }

    public void removeSecondaryTypes(NodeRef nodeRef, List<String> secondaryTypes)
    {
        if (secondaryTypes != null && secondaryTypes.size() > 0)
        {
            for (String secondaryType : secondaryTypes)
            {
                TypeDefinitionWrapper type = getType(secondaryType);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid secondaryType: " + secondaryType);
                }
                nodeService.removeAspect(nodeRef, type.getAlfrescoName());
            }
        }
    }

    private void setAspectProperties(NodeRef nodeRef, boolean isNameChanging, CmisExtensionElement aspectExtension)
    {
        if (aspectExtension.getChildren() == null)
        {
            return;
        }

        List<String> aspectsToAdd = new ArrayList<String>();
        List<String> aspectsToRemove = new ArrayList<String>();
        Map<QName, List<Serializable>> aspectProperties = new HashMap<QName, List<Serializable>>();

        for (CmisExtensionElement extension : aspectExtension.getChildren())
        {
            if (!ALFRESCO_EXTENSION_NAMESPACE.equals(extension.getNamespace()))
            {
                continue;
            }

            if (ASPECTS_TO_ADD.equals(extension.getName()) && (extension.getValue() != null))
            {
                aspectsToAdd.add(extension.getValue());
            }
            else if (ASPECTS_TO_REMOVE.equals(extension.getName()) && (extension.getValue() != null))
            {
                aspectsToRemove.add(extension.getValue());
            }
            else if (PROPERTIES.equals(extension.getName()) && (extension.getChildren() != null))
            {
                for (CmisExtensionElement property : extension.getChildren())
                {
                    if (!property.getName().startsWith("property"))
                    {
                        continue;
                    }

                    String propertyId = (property.getAttributes() == null ? null
                            : property.getAttributes().get(
                                    "propertyDefinitionId"));
                    if ((propertyId == null) || (property.getChildren() == null))
                    {
                        continue;
                    }

                    PropertyType propertyType = PropertyType.STRING;
                    DatatypeFactory df = null;
                    if (property.getName().equals("propertyBoolean"))
                    {
                        propertyType = PropertyType.BOOLEAN;
                    }
                    else if (property.getName().equals("propertyInteger"))
                    {
                        propertyType = PropertyType.INTEGER;
                    }
                    else if (property.getName().equals("propertyDateTime"))
                    {
                        propertyType = PropertyType.DATETIME;
                        try
                        {
                            df = DatatypeFactory.newInstance();
                        }
                        catch (DatatypeConfigurationException e)
                        {
                            throw new CmisRuntimeException("Aspect conversation exception: " + e.getMessage(), e);
                        }
                    }
                    else if (property.getName().equals("propertyDecimal"))
                    {
                        propertyType = PropertyType.DECIMAL;
                    }

                    ArrayList<Serializable> values = new ArrayList<Serializable>();
                    if (property.getChildren() != null)
                    {
                        // try
                        // {
                        for (CmisExtensionElement valueElement : property.getChildren())
                        {
                            if ("value".equals(valueElement.getName()))
                            {
                                switch (propertyType)
                                {
                                case BOOLEAN:
                                    try
                                    {
                                        values.add(Boolean.parseBoolean(valueElement.getValue()));
                                    }
                                    catch (Exception e)
                                    {
                                        throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    }
                                    break;
                                case DATETIME:
                                    try
                                    {
                                        values.add(df.newXMLGregorianCalendar(valueElement.getValue())
                                                .toGregorianCalendar());
                                    }
                                    catch (Exception e)
                                    {
                                        throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    }
                                    break;
                                case INTEGER:
                                    BigInteger value = null;
                                    try
                                    {
                                        value = new BigInteger(valueElement.getValue());
                                    }
                                    catch (Exception e)
                                    {
                                        throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    }

                                    // overflow check
                                    PropertyDefinitionWrapper propDef = getOpenCMISDictionaryService().findProperty(propertyId);
                                    if (propDef == null)
                                    {
                                        throw new CmisInvalidArgumentException("Property " + propertyId + " is unknown!");
                                    }

                                    QName propertyQName = propDef.getPropertyAccessor().getMappedProperty();
                                    if (propertyQName == null)
                                    {
                                        throw new CmisConstraintException("Unable to set property " + propertyId + "!");
                                    }

                                    org.alfresco.service.cmr.dictionary.PropertyDefinition def = dictionaryService.getProperty(propertyQName);
                                    QName dataDef = def.getDataType().getName();

                                    if (dataDef.equals(DataTypeDefinition.INT) && (value.compareTo(maxInt) > 0 || value.compareTo(minInt) < 0))
                                    {
                                        throw new CmisConstraintException("Value is out of range for property " + propertyId);
                                    }

                                    if (dataDef.equals(DataTypeDefinition.LONG) && (value.compareTo(maxLong) > 0 || value.compareTo(minLong) < 0))
                                    {
                                        throw new CmisConstraintException("Value is out of range for property " + propertyId);
                                    }

                                    values.add(value);
                                    break;
                                case DECIMAL:
                                    try
                                    {
                                        values.add(new BigDecimal(valueElement.getValue()));
                                    }
                                    catch (Exception e)
                                    {
                                        throw new CmisInvalidArgumentException("Invalid property aspect value: " + propertyId, e);
                                    }
                                    break;
                                default:
                                    values.add(valueElement.getValue());
                                }
                            }
                        }
                    }

                    aspectProperties.put(QName.createQName(propertyId, namespaceService), values);
                }
            }
        }

        // remove and add aspects
        String aspectType = null;
        try
        {
            for (String aspect : aspectsToRemove)
            {
                aspectType = aspect;

                TypeDefinitionWrapper type = getType(aspect);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
                }

                QName typeName = type.getAlfrescoName();
                // if aspect is hidden aspect, remove only if hidden node is not client controlled
                if (typeName.equals(ContentModel.ASPECT_HIDDEN))
                {
                    if (hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    {
                        // manipulate hidden aspect only if client controlled
                        nodeService.removeAspect(nodeRef, typeName);
                    }

                    // if(!isNameChanging && !hiddenAspect.isClientControlled(nodeRef) && !aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    // {
                    // nodeService.removeAspect(nodeRef, typeName);
                    // }
                }
                else
                {
                    nodeService.removeAspect(nodeRef, typeName);
                }
            }

            for (String aspect : aspectsToAdd)
            {
                aspectType = aspect;

                TypeDefinitionWrapper type = getType(aspect);
                if (type == null)
                {
                    throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
                }

                QName typeName = type.getAlfrescoName();
                // if aspect is hidden aspect, remove only if hidden node is not client controlled
                if (typeName.equals(ContentModel.ASPECT_HIDDEN))
                {
                    if (hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    {
                        // manipulate hidden aspect only if client controlled
                        nodeService.addAspect(nodeRef, type.getAlfrescoName(),
                                Collections.<QName, Serializable> emptyMap());
                    }

                    // if(!isNameChanging && !hiddenAspect.isClientControlled(nodeRef) && !aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    // {
                    // nodeService.addAspect(nodeRef, type.getAlfrescoName(),
                    // Collections.<QName, Serializable> emptyMap());
                    // }
                }
                else
                {
                    nodeService.addAspect(nodeRef, type.getAlfrescoName(),
                            Collections.<QName, Serializable> emptyMap());
                }
            }
        }
        catch (InvalidAspectException e)
        {
            throw new CmisInvalidArgumentException("Invalid aspect: " + aspectType);
        }
        catch (InvalidNodeRefException e)
        {
            throw new CmisInvalidArgumentException("Invalid node: " + nodeRef);
        }

        // set property
        for (Map.Entry<QName, List<Serializable>> property : aspectProperties.entrySet())
        {
            QName propertyQName = property.getKey();

            if (property.getValue().isEmpty())
            {
                if (HiddenAspect.HIDDEN_PROPERTIES.contains(property.getKey()))
                {
                    if (hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    {
                        // manipulate hidden aspect property only if client controlled
                        nodeService.removeProperty(nodeRef, propertyQName);
                    }
                }
                else
                {
                    nodeService.removeProperty(nodeRef, property.getKey());
                }
            }
            else
            {
                if (HiddenAspect.HIDDEN_PROPERTIES.contains(property.getKey()))
                {
                    if (hiddenAspect.isClientControlled(nodeRef) || aspectProperties.containsKey(ContentModel.PROP_CLIENT_CONTROLLED))
                    {
                        // manipulate hidden aspect property only if client controlled
                        nodeService.setProperty(nodeRef, property.getKey(), property.getValue().size() == 1 ? property
                                .getValue().get(0) : (Serializable) property.getValue());
                    }
                }
                else
                {
                    Serializable value = (Serializable) property.getValue();
                    nodeService.setProperty(nodeRef, property.getKey(), property.getValue().size() == 1 ? property
                            .getValue().get(0) : value);
                }
            }
        }
    }

    /**
     * Sets a property value.
     */
    public void setProperty(NodeRef nodeRef, TypeDefinitionWrapper type, String propertyId, Serializable value)
    {
        if (propertyId == null)
        {
            throw new CmisInvalidArgumentException("Cannot process not null property!");
        }

        PropertyDefinitionWrapper propDef = type.getPropertyById(propertyId);
        if (propDef == null)
        {
            throw new CmisInvalidArgumentException("Property " + propertyId + " is unknown!");
        }

        Boolean isOnWorkingCopy = checkOutCheckInService.isWorkingCopy(nodeRef);
        Updatability updatability = propDef.getPropertyDefinition().getUpdatability();
        if (!isUpdatable(updatability, isOnWorkingCopy))
        {
            throw new CmisInvalidArgumentException("Property " + propertyId + " is read-only!");
        }

        if (propDef.getPropertyId().equals(PropertyIds.SECONDARY_OBJECT_TYPE_IDS))
        {
            throw new IllegalArgumentException("Cannot process " + PropertyIds.SECONDARY_OBJECT_TYPE_IDS + " in setProperty");
        }
        else
        {
            QName propertyQName = propDef.getPropertyAccessor().getMappedProperty();
            if (propertyQName == null)
            {
                throw new CmisConstraintException("Unable to set property " + propertyId + "!");
            }

            if (propertyId.equals(PropertyIds.NAME))
            {
                if (!(value instanceof String))
                {
                    throw new CmisInvalidArgumentException("Object name must be a string!");
                }

                try
                {
                    String newName = value.toString();
                    // If the node is checked out and the name property is set on the working copy, make sure the new name has the working copy format
                    if (isOnWorkingCopy)
                    {
                        String wcLabel = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_WORKING_COPY_LABEL);
                        if (wcLabel == null)
                        {
                            wcLabel = CheckOutCheckInServiceImpl.getWorkingCopyLabel();
                        }
                        if (!newName.contains(wcLabel))
                        {
                            newName = CheckOutCheckInServiceImpl.createWorkingCopyName(newName, wcLabel);
                        }
                    }

                    fileFolderService.rename(nodeRef, newName);
                }
                catch (FileExistsException e)
                {
                    throw new CmisContentAlreadyExistsException("An object with this name already exists!", e);
                }
                catch (FileNotFoundException e)
                {
                    throw new CmisInvalidArgumentException("Object with id " + nodeRef.getId() + " not found!");
                }
            }
            else
            {
                // overflow check
                if (propDef.getPropertyDefinition().getPropertyType() == PropertyType.INTEGER && value instanceof BigInteger)
                {
                    org.alfresco.service.cmr.dictionary.PropertyDefinition def = dictionaryService.getProperty(propertyQName);
                    QName dataDef = def.getDataType().getName();
                    BigInteger bigValue = (BigInteger) value;

                    if ((bigValue.compareTo(maxInt) > 0 || bigValue.compareTo(minInt) < 0) && dataDef.equals(DataTypeDefinition.INT))
                    {
                        throw new CmisConstraintException("Value is out of range for property " + propertyQName.getLocalName());
                    }

                    if ((bigValue.compareTo(maxLong) > 0 || bigValue.compareTo(minLong) < 0) && dataDef.equals(DataTypeDefinition.LONG))
                    {
                        throw new CmisConstraintException("Value is out of range for property " + propertyQName.getLocalName());
                    }
                }

                nodeService.setProperty(nodeRef, propertyQName, value);
            }
        }
    }

    private Serializable getValue(PropertyData<?> property, boolean isMultiValue)
    {
        if ((property.getValues() == null) || (property.getValues().isEmpty()))
        {
            return null;
        }

        if (isMultiValue)
        {
            return (Serializable) property.getValues();
        }

        return (Serializable) property.getValues().get(0);
    }

    /**
     * Returns content changes.
     */
    public ObjectList getContentChanges(Holder<String> changeLogToken, BigInteger maxItems)
    {
        final ObjectListImpl result = new ObjectListImpl();
        result.setObjects(new ArrayList<ObjectData>());

        // Collect entryIds to use a counter and a way to find the last changeLogToken
        final List<Long> entryIds = new ArrayList<Long>();

        EntryIdCallback changeLogCollectingCallback = new EntryIdCallback(true) {
            @Override
            public boolean handleAuditEntry(Long entryId, String user, long time, Map<String, Serializable> values)
            {
                entryIds.add(entryId);
                result.getObjects().addAll(createChangeEvents(time, values));
                return super.handleAuditEntry(entryId, user, time, values);
            }
        };

        Long from = null;
        if ((changeLogToken != null) && (changeLogToken.getValue() != null))
        {
            try
            {
                from = Long.parseLong(changeLogToken.getValue());
            }
            catch (NumberFormatException e)
            {
                throw new CmisInvalidArgumentException("Invalid change log token: " + changeLogToken.getValue());
            }
        }

        AuditQueryParameters params = new AuditQueryParameters();
        params.setApplicationName(CMIS_CHANGELOG_AUDIT_APPLICATION);
        params.setForward(true);
        params.setFromId(from);

        // So we have a BigInteger. We need to ensure that we cut it down to an integer smaller than Integer.MAX_VALUE

        int maxResults = (maxItems == null ? contentChangesDefaultMaxItems : maxItems.intValue());
        maxResults = maxResults < 1 ? contentChangesDefaultMaxItems : maxResults; // Just a double check of the unbundled contents
        maxResults = maxResults > contentChangesDefaultMaxItems ? contentChangesDefaultMaxItems : maxResults; // cut it down
        int queryFor = maxResults + 1; // Query for 1 more so that we know if there are more results

        auditService.auditQuery(changeLogCollectingCallback, params, queryFor);

        int resultSize = result.getObjects().size();

        // Use the entryIds as a counter is more reliable then the result.getObjects().
        // result.getObjects() can be more or less then the requested maxResults, because it is filtered based on the content.
        boolean hasMoreItems = entryIds.size() >= maxResults;
        result.setHasMoreItems(hasMoreItems);
        // Check if we got more than the client requested
        if (hasMoreItems && resultSize >= maxResults)
        {
            // We are assuming there are there is only one extra document now in line with how it used to behave
            // Remove extra item that was not actually requested
            result.getObjects().remove(resultSize - 1);
            entryIds.remove(resultSize - 1);
        }

        if (changeLogToken != null)
        {
            // Update the changelog after removing the last item if there are more items.
            Long newChangeLogToken = entryIds.isEmpty() ? from : entryIds.get(entryIds.size() - 1);
            changeLogToken.setValue(String.valueOf(newChangeLogToken));
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    private List<ObjectData> createChangeEvents(long time, Map<String, Serializable> values)
    {
        List<ObjectData> result = new ArrayList<ObjectData>();

        if ((values == null) || (values.size() == 0))
        {
            return result;
        }

        GregorianCalendar changeTime = new GregorianCalendar();
        changeTime.setTimeInMillis(time);

        String appPath = "/" + CMIS_CHANGELOG_AUDIT_APPLICATION + "/";

        for (Entry<String, Serializable> entry : values.entrySet())
        {
            if ((entry.getKey() == null) || (!(entry.getValue() instanceof Map)))
            {
                continue;
            }

            String path = entry.getKey();
            if (!path.startsWith(appPath))
            {
                continue;
            }

            ChangeType changeType = null;
            String changePath = path.substring(appPath.length()).toLowerCase();
            for (ChangeType c : ChangeType.values())
            {
                if (changePath.startsWith(c.value().toLowerCase()))
                {
                    changeType = c;
                    break;
                }
            }

            if (changeType == null)
            {
                continue;
            }

            Map<String, Serializable> valueMap = (Map<String, Serializable>) entry.getValue();
            String objectId = (String) valueMap.get(CMISChangeLogDataExtractor.KEY_OBJECT_ID);

            // build object
            ObjectDataImpl object = new ObjectDataImpl();
            result.add(object);

            PropertiesImpl properties = new PropertiesImpl();
            object.setProperties(properties);
            PropertyIdImpl objectIdProperty = new PropertyIdImpl(PropertyIds.OBJECT_ID, objectId);
            properties.addProperty(objectIdProperty);

            ChangeEventInfoDataImpl changeEvent = new ChangeEventInfoDataImpl();
            object.setChangeEventInfo(changeEvent);
            changeEvent.setChangeType(changeType);
            changeEvent.setChangeTime(changeTime);
        }

        return result;
    }

    private class EntryIdCallback implements AuditQueryCallback
    {
        private final boolean valuesRequired;
        private Long entryId;

        public EntryIdCallback(boolean valuesRequired)
        {
            this.valuesRequired = valuesRequired;
        }

        public String getEntryId()
        {
            return entryId == null ? null : entryId.toString();
        }

        public boolean valuesRequired()
        {
            return this.valuesRequired;
        }

        public final boolean handleAuditEntry(Long entryId, String applicationName, String user, long time,
                Map<String, Serializable> values)
        {
            if (applicationName.equals(CMIS_CHANGELOG_AUDIT_APPLICATION))
            {
                return handleAuditEntry(entryId, user, time, values);
            }
            return true;
        }

        public boolean handleAuditEntry(Long entryId, String user, long time, Map<String, Serializable> values)
        {
            this.entryId = entryId;
            return true;
        }

        public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
        {
            throw new CmisRuntimeException("Audit entry " + entryId + ": " + errorMsg, error);
        }
    };

    // --------------------------------------------------------------
    // OpenCMIS methods
    // --------------------------------------------------------------

    /**
     * Returns the value of the given property if it exists and is of the correct type.
     */
    public String getStringProperty(Properties properties, String propertyId)
    {
        if ((properties == null) || (properties.getProperties() == null))
        {
            return null;
        }

        PropertyData<?> property = properties.getProperties().get(propertyId);
        if (!(property instanceof PropertyString))
        {
            return null;
        }

        return ((PropertyString) property).getFirstValue();
    }

    /**
     * Returns the value of the given property if it exists and is of the correct type.
     */
    public String getIdProperty(Properties properties, String propertyId)
    {
        if ((properties == null) || (properties.getProperties() == null))
        {
            return null;
        }

        PropertyData<?> property = properties.getProperties().get(propertyId);
        if (!(property instanceof PropertyId))
        {
            return null;
        }

        return ((PropertyId) property).getFirstValue();
    }

    public String getNameProperty(Properties properties, String fallback)
    {
        String name = getStringProperty(properties, PropertyIds.NAME);
        if ((name == null) || (name.trim().length() == 0))
        {
            if (fallback == null)
            {
                throw new CmisInvalidArgumentException("Property " + PropertyIds.NAME + " must be set!");
            }
            else
            {
                name = fallback;
            }
        }

        return name;
    }

    public String getObjectTypeIdProperty(Properties properties)
    {
        String objectTypeId = getIdProperty(properties, PropertyIds.OBJECT_TYPE_ID);
        if ((objectTypeId == null) || (objectTypeId.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.OBJECT_TYPE_ID + " must be set!");
        }

        return objectTypeId;
    }

    public String getSourceIdProperty(Properties properties)
    {
        String id = getIdProperty(properties, PropertyIds.SOURCE_ID);
        if ((id == null) || (id.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.SOURCE_ID + " must be set!");
        }

        return id;
    }

    public String getTargetIdProperty(Properties properties)
    {
        String id = getIdProperty(properties, PropertyIds.TARGET_ID);
        if ((id == null) || (id.trim().length() == 0))
        {
            throw new CmisInvalidArgumentException("Property " + PropertyIds.TARGET_ID + " must be set!");
        }

        return id;
    }

    /**
     * Returns the repository info object.
     */
    public RepositoryInfo getRepositoryInfo(CmisVersion cmisVersion)
    {
        return createRepositoryInfo(cmisVersion);
    }

    /**
     * Returns the repository id.
     */
    public String getRepositoryId()
    {
        return descriptorService.getCurrentRepositoryDescriptor().getId();
    }

    /**
     * Creates the repository info object.
     */
    private RepositoryInfo createRepositoryInfo(CmisVersion cmisVersion)
    {
        Descriptor currentDescriptor = descriptorService.getCurrentRepositoryDescriptor();

        // get change token
        boolean auditEnabled = auditService.isAuditEnabled(CMIS_CHANGELOG_AUDIT_APPLICATION, "/"
                + CMIS_CHANGELOG_AUDIT_APPLICATION);
        String latestChangeLogToken = null;

        if (auditEnabled)
        {
            EntryIdCallback auditQueryCallback = new EntryIdCallback(false);
            AuditQueryParameters params = new AuditQueryParameters();
            params.setApplicationName(CMIS_CHANGELOG_AUDIT_APPLICATION);
            params.setForward(false);
            auditService.auditQuery(auditQueryCallback, params, 1);
            String entryId = auditQueryCallback.getEntryId();
            // MNT-13529
            // add initial change log token
            latestChangeLogToken = entryId == null ? "0" : entryId;
        }

        // compile repository info
        RepositoryInfoImpl ri = new RepositoryInfoImpl();

        ri.setId(currentDescriptor.getId());
        ri.setName(currentDescriptor.getName());
        ri.setDescription(currentDescriptor.getName());
        ri.setVendorName("Alfresco");
        ri.setProductName("Alfresco " + descriptorService.getServerDescriptor().getEdition());
        ri.setProductVersion(currentDescriptor.getVersion());
        NodeRef rootNodeRef = getRootNodeRef();
        ri.setRootFolder(constructObjectId(rootNodeRef, null));
        ri.setCmisVersion(cmisVersion);

        ri.setChangesIncomplete(true);
        ri.setChangesOnType(Arrays.asList(new BaseTypeId[]{BaseTypeId.CMIS_DOCUMENT, BaseTypeId.CMIS_FOLDER}));
        ri.setLatestChangeLogToken(latestChangeLogToken);
        ri.setPrincipalAnonymous(AuthenticationUtil.getGuestUserName());
        ri.setPrincipalAnyone(PermissionService.ALL_AUTHORITIES);

        RepositoryCapabilitiesImpl repCap = new RepositoryCapabilitiesImpl();
        ri.setCapabilities(repCap);

        repCap.setAllVersionsSearchable(false);
        repCap.setCapabilityAcl(CapabilityAcl.MANAGE);
        repCap.setCapabilityChanges(auditEnabled ? CapabilityChanges.OBJECTIDSONLY : CapabilityChanges.NONE);
        repCap.setCapabilityContentStreamUpdates(CapabilityContentStreamUpdates.ANYTIME);
        repCap.setCapabilityJoin(CapabilityJoin.NONE);
        repCap.setCapabilityQuery(CapabilityQuery.BOTHCOMBINED);
        repCap.setCapabilityRendition(CapabilityRenditions.READ);
        repCap.setIsPwcSearchable(false);
        repCap.setIsPwcUpdatable(true);
        repCap.setSupportsGetDescendants(true);
        repCap.setSupportsGetFolderTree(true);
        repCap.setSupportsMultifiling(true);
        repCap.setSupportsUnfiling(false);
        repCap.setSupportsVersionSpecificFiling(false);

        AclCapabilitiesDataImpl aclCap = new AclCapabilitiesDataImpl();
        ri.setAclCapabilities(aclCap);

        aclCap.setAclPropagation(AclPropagation.PROPAGATE);
        aclCap.setSupportedPermissions(SupportedPermissions.BOTH);
        aclCap.setPermissionDefinitionData(repositoryPermissions);
        aclCap.setPermissionMappingData(permissionMappings);

        return ri;
    }

    private List<PermissionDefinition> getRepositoryPermissions()
    {
        ArrayList<PermissionDefinition> result = new ArrayList<PermissionDefinition>();

        Set<PermissionReference> all = permissionModelDao.getAllPermissions();
        for (PermissionReference pr : all)
        {
            result.add(createPermissionDefinition(pr));
        }

        PermissionReference allPermission = permissionModelDao.getPermissionReference(null,
                PermissionService.ALL_PERMISSIONS);
        result.add(createPermissionDefinition(allPermission));

        PermissionDefinitionDataImpl cmisPermission;

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.READ);
        cmisPermission.setDescription("CMIS Read");
        result.add(cmisPermission);

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.WRITE);
        cmisPermission.setDescription("CMIS Write");
        result.add(cmisPermission);

        cmisPermission = new PermissionDefinitionDataImpl();
        cmisPermission.setId(BasicPermissions.ALL);
        cmisPermission.setDescription("CMIS All");
        result.add(cmisPermission);

        return result;
    }

    private PermissionDefinition createPermissionDefinition(PermissionReference pr)
    {
        PermissionDefinitionDataImpl permission = new PermissionDefinitionDataImpl();
        permission.setId(pr.getQName().toString() + "." + pr.getName());
        permission.setDescription(permission.getId());

        return permission;
    }

    private Map<String, PermissionMapping> getPermissionMappings()
    {
        Map<String, PermissionMapping> result = new HashMap<String, PermissionMapping>();

        for (CMISAllowedActionEnum e : EnumSet.allOf(CMISAllowedActionEnum.class))
        {
            for (Map.Entry<String, List<String>> m : e.getPermissionMapping().entrySet())
            {
                PermissionMappingDataImpl mapping = new PermissionMappingDataImpl();
                mapping.setKey(m.getKey());
                mapping.setPermissions(m.getValue());

                result.put(mapping.getKey(), mapping);
            }
        }

        return result;
    }

    /**
     *
     * @deprecated The RenditionService is being replace by the simpler async RenditionService2.
     */
    @Deprecated
    private CMISRenditionMapping getRenditionMapping()
    {
        CMISRenditionMapping renditionMapping = (CMISRenditionMapping) singletonCache.get(KEY_CMIS_RENDITION_MAPPING_NODEREF);
        if (renditionMapping == null)
        {
            renditionMapping = new CMISRenditionMapping(nodeService, contentService, renditionService,
                    transactionService, kindToRenditionNames);

            singletonCache.put(KEY_CMIS_RENDITION_MAPPING_NODEREF, renditionMapping);
        }
        return renditionMapping;
    }

    /**
     * Verify if a property is updatable.
     * 
     * @param updatability
     * @param isOnWorkingCopy
     * @return
     */
    private boolean isUpdatable(Updatability updatability, Boolean isOnWorkingCopy)
    {
        if ((updatability == Updatability.READONLY)
                || (updatability == Updatability.WHENCHECKEDOUT && !isOnWorkingCopy))
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}
